From 442e533803c100b531b4cb505ccb64173bfd48c8 Mon Sep 17 00:00:00 2001 From: Guilherme Caulada Date: Mon, 23 Oct 2023 20:14:12 -0300 Subject: [PATCH 001/180] CI: Rename scripts that build artifacts to use _build_ (#77005) Rename scripts that build artifacts to use _build_ --- .drone.yml | 8 ++++---- scripts/drone/rgm.star | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.drone.yml b/.drone.yml index 8af3f0826ad..ed10ff276f3 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2771,7 +2771,7 @@ services: [] steps: - commands: - export GRAFANA_DIR=$$(pwd) - - cd /src && ./scripts/drone_publish_main.sh + - cd /src && ./scripts/drone_build_main.sh environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token @@ -3020,7 +3020,7 @@ services: [] steps: - commands: - export GRAFANA_DIR=$$(pwd) - - cd /src && ./scripts/drone_publish_tag_grafana.sh + - cd /src && ./scripts/drone_build_tag_grafana.sh environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token @@ -3198,7 +3198,7 @@ services: [] steps: - commands: - export GRAFANA_DIR=$$(pwd) - - cd /src && ./scripts/drone_publish_tag_grafana.sh + - cd /src && ./scripts/drone_build_tag_grafana.sh environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token @@ -4607,6 +4607,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: a7032573772937b59787f5c01c113b02afb07a9febf15664a68cbcee1458acc9 +hmac: a23a1640b21a7e8dd62089ab21b84a4efc43504cd6e956dd0f49e56df4f4e5a2 ... diff --git a/scripts/drone/rgm.star b/scripts/drone/rgm.star index cd4df9b9de8..411d4a4c969 100644 --- a/scripts/drone/rgm.star +++ b/scripts/drone/rgm.star @@ -221,7 +221,7 @@ def rgm_main(): return pipeline( name = "rgm-main-prerelease", trigger = main_trigger, - steps = rgm_run("rgm-build", "drone_publish_main.sh"), + steps = rgm_run("rgm-build", "drone_build_main.sh"), depends_on = ["main-test-backend", "main-test-frontend"], ) @@ -230,7 +230,7 @@ def rgm_tag(): return pipeline( name = "rgm-tag-prerelease", trigger = tag_trigger, - steps = rgm_run("rgm-build", "drone_publish_tag_grafana.sh"), + steps = rgm_run("rgm-build", "drone_build_tag_grafana.sh"), depends_on = ["release-test-backend", "release-test-frontend"], ) @@ -251,7 +251,7 @@ def rgm_version_branch(): return pipeline( name = "rgm-version-branch-prerelease", trigger = version_branch_trigger, - steps = rgm_run("rgm-build", "drone_publish_tag_grafana.sh"), + steps = rgm_run("rgm-build", "drone_build_tag_grafana.sh"), depends_on = ["release-test-backend", "release-test-frontend"], ) From 03a626f1d6399c2300cc420c0b5aebc5a9af43be Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Tue, 24 Oct 2023 10:04:45 +0300 Subject: [PATCH 002/180] Search: Fix empty folder details for nested folder items (#76504) * Introduce dashboard.folder_uid column * Add data migration * Search: Fix empty folder details for nested folders * Set `dashboard.folder_uid` and update tests * Add unique index * lint Ignore cyclomatic complexity of func `(*DashboardServiceImpl).BuildSaveDashboardCommand * Fix search by folder UID --- pkg/api/dashboard.go | 13 +- pkg/api/dashboard_test.go | 19 +- pkg/api/folder.go | 2 +- pkg/services/alerting/store_test.go | 13 +- .../dashboardimport/service/service.go | 1 + pkg/services/dashboards/database/acl_test.go | 4 +- pkg/services/dashboards/database/database.go | 8 +- .../database/database_folder_test.go | 25 +- .../database/database_provisioning_test.go | 21 +- .../dashboards/database/database_test.go | 420 ++++++++++++++---- .../migrations/folder_uid_migrator.go | 81 ++++ pkg/services/dashboards/models.go | 11 +- pkg/services/dashboards/models_test.go | 2 +- .../dashboards/service/dashboard_service.go | 2 + .../dashboard_service_integration_test.go | 44 +- .../dashverimpl/store_test.go | 16 +- .../folderimpl/dashboard_folder_store_test.go | 24 +- pkg/services/folder/folderimpl/folder.go | 56 ++- pkg/services/folder/folderimpl/folder_test.go | 18 +- .../publicdashboards/api/query_test.go | 7 +- .../database/database_test.go | 49 +- .../publicdashboards/service/query_test.go | 12 +- .../publicdashboards/service/service_test.go | 31 +- .../sqlstore/migrations/migrations.go | 3 + .../sqlstore/permissions/dashboard_test.go | 10 +- pkg/services/sqlstore/searchstore/builder.go | 28 +- pkg/services/sqlstore/searchstore/filters.go | 13 +- .../sqlstore/searchstore/search_test.go | 9 +- 28 files changed, 706 insertions(+), 236 deletions(-) create mode 100644 pkg/services/dashboards/database/migrations/folder_uid_migrator.go diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 60f6e27a528..91b38518335 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -412,10 +412,13 @@ func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.S cmd.OrgID = c.SignedInUser.GetOrgID() cmd.UserID = userID - if cmd.FolderUID != "" { + // nolint:staticcheck + if cmd.FolderUID != "" || cmd.FolderID != 0 { folder, err := hs.folderService.Get(ctx, &folder.GetFolderQuery{ - OrgID: c.SignedInUser.GetOrgID(), - UID: &cmd.FolderUID, + OrgID: c.SignedInUser.GetOrgID(), + UID: &cmd.FolderUID, + // nolint:staticcheck + ID: &cmd.FolderID, SignedInUser: c.SignedInUser, }) if err != nil { @@ -424,7 +427,9 @@ func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.S } return response.Error(http.StatusInternalServerError, "Error while checking folder ID", err) } + // nolint:staticcheck cmd.FolderID = folder.ID + cmd.FolderUID = folder.UID } dash := cmd.GetDashboardModel() @@ -1073,7 +1078,9 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *contextmodel.ReqContext) respon saveCmd.Dashboard.Set("version", dash.Version) saveCmd.Dashboard.Set("uid", dash.UID) saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version) + // nolint:staticcheck saveCmd.FolderID = dash.FolderID + saveCmd.FolderUID = dash.FolderUID return hs.postDashboard(c, saveCmd) } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 3d36073a946..50471218474 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -394,6 +394,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { // This tests that a valid request returns correct response t.Run("Given a correct request for creating a dashboard", func(t *testing.T) { const folderID int64 = 3 + folderUID := "Folder" const dashID int64 = 2 cmd := dashboards.SaveDashboardCommand{ @@ -404,15 +405,19 @@ func TestDashboardAPIEndpoint(t *testing.T) { }), Overwrite: true, FolderID: folderID, + FolderUID: folderUID, IsFolder: false, Message: "msg", } dashboardService := dashboards.NewFakeDashboardService(t) dashboardService.On("SaveDashboard", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), mock.AnythingOfType("bool")). - Return(&dashboards.Dashboard{ID: dashID, UID: "uid", Title: "Dash", Slug: "dash", Version: 2}, nil) + Return(&dashboards.Dashboard{ID: dashID, UID: "uid", Title: "Dash", Slug: "dash", Version: 2, FolderUID: folderUID, FolderID: folderID}, nil) + mockFolderService := &foldertest.FakeService{ + ExpectedFolder: &folder.Folder{ID: 1, UID: folderUID, Title: "Folder"}, + } - postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, dashboardService, nil, func(sc *scenarioContext) { + postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, dashboardService, mockFolderService, func(sc *scenarioContext) { callPostDashboardShouldReturnSuccess(sc) result := sc.ToJSON() @@ -664,6 +669,12 @@ func TestDashboardAPIEndpoint(t *testing.T) { Data: fakeDash.Data, }} mockSQLStore := dbtest.NewFakeDB() + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) + t.Cleanup(func() { + guardian.New = origNewGuardian + }) + restoreDashboardVersionScenario(t, "When calling POST on", "/api/dashboards/id/1/restore", "/api/dashboards/id/:dashboardId/restore", dashboardService, fakeDashboardVersionService, cmd, func(sc *scenarioContext) { sc.dashboardVersionService = fakeDashboardVersionService @@ -1084,6 +1095,9 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc, sqlStore db.DB) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { cfg := setting.NewCfg() + folderSvc := foldertest.NewFakeService() + folderSvc.ExpectedFolder = &folder.Folder{} + hs := HTTPServer{ Cfg: cfg, ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()), @@ -1097,6 +1111,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout dashboardVersionService: fakeDashboardVersionService, Kinds: corekind.NewBase(nil), accesscontrolService: actest.FakeService{}, + folderService: folderSvc, } sc := setupScenarioContext(t, url) diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 8a11618cfe3..4e93eeb066d 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -224,7 +224,7 @@ func (hs *HTTPServer) MoveFolder(c *contextmodel.ReqContext) response.Response { cmd.SignedInUser = c.SignedInUser theFolder, err := hs.folderService.Move(c.Req.Context(), &cmd) if err != nil { - return response.Error(http.StatusInternalServerError, "move folder failed", err) + return response.ErrOrFallback(http.StatusInternalServerError, "move folder failed", err) } folderDTO, err := hs.newToFolderDto(c, theFolder) diff --git a/pkg/services/alerting/store_test.go b/pkg/services/alerting/store_test.go index d7d6d9ddbe3..9124125414e 100644 --- a/pkg/services/alerting/store_test.go +++ b/pkg/services/alerting/store_test.go @@ -58,7 +58,7 @@ func TestIntegrationAlertingDataAccess(t *testing.T) { features: featuremgmt.WithFeatures(), } - testDash = insertTestDashboard(t, store.db, "dashboard with alerts", 1, 0, false, "alert") + testDash = insertTestDashboard(t, store.db, "dashboard with alerts", 1, 0, "", false, "alert") evalData, err := simplejson.NewJson([]byte(`{"test": "test"}`)) require.Nil(t, err) items = []*models.Alert{ @@ -337,7 +337,7 @@ func TestIntegrationPausingAlerts(t *testing.T) { cfg := setting.NewCfg() sqlStore := sqlStore{db: ss, cfg: cfg, log: log.New(), tagService: tagimpl.ProvideService(ss, ss.Cfg), features: featuremgmt.WithFeatures()} - testDash := insertTestDashboard(t, sqlStore.db, "dashboard with alerts", 1, 0, false, "alert") + testDash := insertTestDashboard(t, sqlStore.db, "dashboard with alerts", 1, 0, "", false, "alert") alert, err := insertTestAlert("Alerting title", "Alerting message", testDash.OrgID, testDash.ID, simplejson.New(), sqlStore) require.Nil(t, err) @@ -435,12 +435,13 @@ func (ss *sqlStore) pauseAllAlerts(t *testing.T, pauseState bool) error { } func insertTestDashboard(t *testing.T, store db.DB, title string, orgId int64, - folderId int64, isFolder bool, tags ...any) *dashboards.Dashboard { + folderId int64, folderUID string, isFolder bool, tags ...any) *dashboards.Dashboard { t.Helper() cmd := dashboards.SaveDashboardCommand{ - OrgID: orgId, - FolderID: folderId, - IsFolder: isFolder, + OrgID: orgId, + FolderID: folderId, + FolderUID: folderUID, + IsFolder: isFolder, Dashboard: simplejson.NewFromAny(map[string]any{ "id": nil, "title": title, diff --git a/pkg/services/dashboardimport/service/service.go b/pkg/services/dashboardimport/service/service.go index ff53ac0d7f0..63dd1b1e53a 100644 --- a/pkg/services/dashboardimport/service/service.go +++ b/pkg/services/dashboardimport/service/service.go @@ -121,6 +121,7 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb Overwrite: req.Overwrite, PluginID: req.PluginId, FolderID: req.FolderId, + FolderUID: req.FolderUid, } dto := &dashboards.SaveDashboardDTO{ diff --git a/pkg/services/dashboards/database/acl_test.go b/pkg/services/dashboards/database/acl_test.go index 92043205c61..1c7bebe1153 100644 --- a/pkg/services/dashboards/database/acl_test.go +++ b/pkg/services/dashboards/database/acl_test.go @@ -38,8 +38,8 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { dashboardStore, err = ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) currentUser = createUser(t, sqlStore, "viewer", "Viewer", false) - savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp") - childDash = insertTestDashboard(t, dashboardStore, "2 test dash", 1, savedFolder.ID, false, "prod", "webapp") + savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, "", true, "prod", "webapp") + childDash = insertTestDashboard(t, dashboardStore, "2 test dash", 1, savedFolder.ID, savedFolder.UID, false, "prod", "webapp") return currentUser.OrgID } diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index d959cd280d8..b04453bc1b1 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -468,7 +468,7 @@ func saveDashboard(sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitE dash.Updated = time.Now() dash.UpdatedBy = userId metrics.MApiDashboardInsert.Inc() - affectedRows, err = sess.Insert(dash) + affectedRows, err = sess.Nullable("folder_uid").Insert(dash) } else { dash.SetVersion(dash.Version + 1) @@ -480,7 +480,7 @@ func saveDashboard(sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitE dash.UpdatedBy = userId - affectedRows, err = sess.MustCols("folder_id").ID(dash.ID).Update(dash) + affectedRows, err = sess.MustCols("folder_id", "folder_uid").Nullable("folder_uid").ID(dash.ID).Update(dash) } if err != nil { @@ -1007,11 +1007,11 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F } if len(query.FolderUIDs) > 0 { - filters = append(filters, searchstore.FolderUIDFilter{Dialect: d.store.GetDialect(), OrgID: orgID, UIDs: query.FolderUIDs}) + filters = append(filters, searchstore.FolderUIDFilter{Dialect: d.store.GetDialect(), OrgID: orgID, UIDs: query.FolderUIDs, NestedFoldersEnabled: d.features.IsEnabled(featuremgmt.FlagNestedFolders)}) } var res []dashboards.DashboardSearchProjection - sb := &searchstore.Builder{Dialect: d.store.GetDialect(), Filters: filters} + sb := &searchstore.Builder{Dialect: d.store.GetDialect(), Filters: filters, Features: d.features} limit := query.Limit if limit < 1 { diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go index 6478fa628ef..482f515717a 100644 --- a/pkg/services/dashboards/database/database_folder_test.go +++ b/pkg/services/dashboards/database/database_folder_test.go @@ -49,10 +49,10 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { var err error dashboardStore, err = ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) - flder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp") - dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod", "webapp") - childDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, flder.ID, false, "prod", "webapp") - insertTestDashboard(t, dashboardStore, "test dash 45", 1, flder.ID, false, "prod") + flder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, "", true, "prod", "webapp") + dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, "", false, "prod", "webapp") + childDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, flder.ID, flder.UID, false, "prod", "webapp") + insertTestDashboard(t, dashboardStore, "test dash 45", 1, flder.ID, flder.UID, false, "prod") currentUser = &user.SignedInUser{ UserID: 1, OrgID: 1, @@ -147,11 +147,11 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { var err error dashboardStore, err = ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) - folder1 = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod") - folder2 = insertTestDashboard(t, dashboardStore, "2 test dash folder", 1, 0, true, "prod") - dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod") - childDash1 = insertTestDashboard(t, dashboardStore, "child dash 1", 1, folder1.ID, false, "prod") - childDash2 = insertTestDashboard(t, dashboardStore, "child dash 2", 1, folder2.ID, false, "prod") + folder1 = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, "", true, "prod") + folder2 = insertTestDashboard(t, dashboardStore, "2 test dash folder", 1, 0, "", true, "prod") + dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, "", false, "prod") + childDash1 = insertTestDashboard(t, dashboardStore, "child dash 1", 1, folder1.ID, folder1.UID, false, "prod") + childDash2 = insertTestDashboard(t, dashboardStore, "child dash 2", 1, folder2.ID, folder2.UID, false, "prod") currentUser = &user.SignedInUser{ UserID: 1, @@ -186,7 +186,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("and acl is set for one dashboard folder", func(t *testing.T) { t.Run("and a dashboard is moved from folder without acl to the folder with an acl", func(t *testing.T) { - moveDashboard(t, dashboardStore, 1, childDash2.Data, folder1.ID) + moveDashboard(t, dashboardStore, 1, childDash2.Data, folder1.ID, folder1.UID) currentUser.Permissions = map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2.UID), dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashInRoot.UID)}}} actest.AddUserPermissionToDB(t, sqlStore, currentUser) @@ -204,7 +204,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { }) t.Run("and a dashboard is moved from folder with acl to the folder without an acl", func(t *testing.T) { setup2() - moveDashboard(t, dashboardStore, 1, childDash1.Data, folder2.ID) + moveDashboard(t, dashboardStore, 1, childDash1.Data, folder2.ID, folder2.UID) currentUser.Permissions = map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashInRoot.UID), dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2.UID)}, dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2.UID)}}} actest.AddUserPermissionToDB(t, sqlStore, currentUser) @@ -435,12 +435,13 @@ func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) { } func moveDashboard(t *testing.T, dashboardStore dashboards.Store, orgId int64, dashboard *simplejson.Json, - newFolderId int64) *dashboards.Dashboard { + newFolderId int64, newFolderUID string) *dashboards.Dashboard { t.Helper() cmd := dashboards.SaveDashboardCommand{ OrgID: orgId, FolderID: newFolderId, + FolderUID: newFolderUID, Dashboard: dashboard, Overwrite: true, } diff --git a/pkg/services/dashboards/database/database_provisioning_test.go b/pkg/services/dashboards/database/database_provisioning_test.go index 01132de37c3..8baca27e876 100644 --- a/pkg/services/dashboards/database/database_provisioning_test.go +++ b/pkg/services/dashboards/database/database_provisioning_test.go @@ -24,9 +24,10 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { require.NoError(t, err) folderCmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - FolderID: 0, - IsFolder: true, + OrgID: 1, + FolderID: 0, + FolderUID: "", + IsFolder: true, Dashboard: simplejson.NewFromAny(map[string]any{ "id": nil, "title": "test dashboard", @@ -37,9 +38,10 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { require.Nil(t, err) saveDashboardCmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - IsFolder: false, - FolderID: dash.ID, + OrgID: 1, + IsFolder: false, + FolderID: dash.ID, + FolderUID: dash.UID, Dashboard: simplejson.NewFromAny(map[string]any{ "id": nil, "title": "test dashboard", @@ -63,9 +65,10 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { t.Run("Deleting orphaned provisioned dashboards", func(t *testing.T) { saveCmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - IsFolder: false, - FolderID: dash.ID, + OrgID: 1, + IsFolder: false, + FolderID: dash.ID, + FolderUID: dash.UID, Dashboard: simplejson.NewFromAny(map[string]any{ "id": nil, "title": "another_dashboard", diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index 88de8d1460f..822cdc12acf 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -11,11 +11,17 @@ 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/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/guardian" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/search/model" @@ -42,10 +48,10 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { var err error dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) - savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp") - savedDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp") - insertTestDashboard(t, dashboardStore, "test dash 45", 1, savedFolder.ID, false, "prod") - savedDash2 = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod") + savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, "", true, "prod", "webapp") + savedDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, savedFolder.ID, savedFolder.UID, false, "prod", "webapp") + insertTestDashboard(t, dashboardStore, "test dash 45", 1, savedFolder.ID, savedFolder.UID, false, "prod") + savedDash2 = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, "", false, "prod") insertTestRule(t, sqlStore, savedFolder.OrgID, savedFolder.UID) } @@ -174,7 +180,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Should be able to delete dashboard", func(t *testing.T) { setup() - dash := insertTestDashboard(t, dashboardStore, "delete me", 1, 0, false, "delete this") + dash := insertTestDashboard(t, dashboardStore, "delete me", 1, 0, "", false, "delete this") err := dashboardStore.DeleteDashboard(context.Background(), &dashboards.DeleteDashboardCommand{ ID: dash.ID, @@ -248,7 +254,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Should be able to delete empty folder", func(t *testing.T) { setup() - emptyFolder := insertTestDashboard(t, dashboardStore, "2 test dash folder", 1, 0, true, "prod", "webapp") + emptyFolder := insertTestDashboard(t, dashboardStore, "2 test dash folder", 1, 0, "", true, "prod", "webapp") deleteCmd := &dashboards.DeleteDashboardCommand{ID: emptyFolder.ID} err := dashboardStore.DeleteDashboard(context.Background(), deleteCmd) @@ -522,9 +528,9 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Can delete dashboards in folder", func(t *testing.T) { setup() - folder := insertTestDashboard(t, dashboardStore, "dash folder", 1, 0, true, "prod", "webapp") - _ = insertTestDashboard(t, dashboardStore, "delete me 1", 1, folder.ID, false, "delete this 1") - _ = insertTestDashboard(t, dashboardStore, "delete me 2", 1, folder.ID, false, "delete this 2") + folder := insertTestDashboard(t, dashboardStore, "dash folder", 1, 0, "", true, "prod", "webapp") + _ = insertTestDashboard(t, dashboardStore, "delete me 1", 1, folder.ID, folder.UID, false, "delete this 1") + _ = insertTestDashboard(t, dashboardStore, "delete me 2", 1, folder.ID, folder.UID, false, "delete this 2") err := dashboardStore.DeleteDashboardsInFolder( context.Background(), @@ -577,8 +583,8 @@ func TestIntegrationDashboard_SortingOptions(t *testing.T) { dashboardStore, err := ProvideDashboardStore(sqlStore, &setting.Cfg{}, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) - dashB := insertTestDashboard(t, dashboardStore, "Beta", 1, 0, false) - dashA := insertTestDashboard(t, dashboardStore, "Alfa", 1, 0, false) + dashB := insertTestDashboard(t, dashboardStore, "Beta", 1, 0, "", false) + dashA := insertTestDashboard(t, dashboardStore, "Alfa", 1, 0, "", false) assert.NotZero(t, dashA.ID) assert.Less(t, dashB.ID, dashA.ID) qNoSort := &dashboards.FindPersistedDashboardsQuery{ @@ -629,8 +635,8 @@ func TestIntegrationDashboard_Filter(t *testing.T) { quotaService := quotatest.New(false, nil) dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) - insertTestDashboard(t, dashboardStore, "Alfa", 1, 0, false) - dashB := insertTestDashboard(t, dashboardStore, "Beta", 1, 0, false) + insertTestDashboard(t, dashboardStore, "Alfa", 1, 0, "", false) + dashB := insertTestDashboard(t, dashboardStore, "Beta", 1, 0, "", false) qNoFilter := &dashboards.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{ OrgID: 1, @@ -674,7 +680,7 @@ func TestGetExistingDashboardByTitleAndFolder(t *testing.T) { quotaService := quotatest.New(false, nil) dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) - insertTestDashboard(t, dashboardStore, "Apple", 1, 0, false) + insertTestDashboard(t, dashboardStore, "Apple", 1, 0, "", false) t.Run("Finds a dashboard with existing name in root directory and throws DashboardWithSameNameInFolderExists error", func(t *testing.T) { err = sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { _, err = getExistingDashboardByTitleAndFolder(sess, &dashboards.Dashboard{Title: "Apple", OrgID: 1}, sqlStore.GetDialect(), false, false) @@ -692,8 +698,8 @@ func TestGetExistingDashboardByTitleAndFolder(t *testing.T) { }) t.Run("Finds a dashboard with existing name in specific folder and throws DashboardWithSameNameInFolderExists error", func(t *testing.T) { - savedFolder := insertTestDashboard(t, dashboardStore, "test dash folder", 1, 0, true, "prod", "webapp") - savedDash := insertTestDashboard(t, dashboardStore, "test dash", 1, savedFolder.ID, false, "prod", "webapp") + savedFolder := insertTestDashboard(t, dashboardStore, "test dash folder", 1, 0, "", true, "prod", "webapp") + savedDash := insertTestDashboard(t, dashboardStore, "test dash", 1, savedFolder.ID, savedFolder.UID, false, "prod", "webapp") err = sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { _, err = getExistingDashboardByTitleAndFolder(sess, &dashboards.Dashboard{Title: savedDash.Title, FolderID: savedFolder.ID, OrgID: 1}, sqlStore.GetDialect(), false, false) return err @@ -702,6 +708,122 @@ func TestGetExistingDashboardByTitleAndFolder(t *testing.T) { }) } +func TestIntegrationFindDashboardsByTitle(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + sqlStore := db.InitTestDB(t) + cfg := setting.NewCfg() + cfg.IsFeatureToggleEnabled = func(key string) bool { return false } + quotaService := quotatest.New(false, nil) + features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPanelTitleSearch) + dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore, cfg), quotaService) + require.NoError(t, err) + + orgID := int64(1) + insertTestDashboard(t, dashboardStore, "dashboard under general", orgID, 0, "", false) + + ac := acimpl.ProvideAccessControl(sqlStore.Cfg) + folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) + folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, folderStore, sqlStore, features) + + user := &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{ + orgID: { + dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll}, + dashboards.ActionFoldersRead: []string{dashboards.ScopeFoldersAll}, + dashboards.ActionFoldersWrite: []string{dashboards.ScopeFoldersAll}, + }, + }, + } + + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanSaveValue: true, + CanViewValue: true, + // CanEditValue is required to create library elements + CanEditValue: true, + }) + t.Cleanup(func() { + guardian.New = origNewGuardian + }) + + f0, err := folderServiceWithFlagOn.Create(context.Background(), &folder.CreateFolderCommand{ + OrgID: orgID, + Title: "f0", + SignedInUser: user, + }) + require.NoError(t, err) + insertTestDashboard(t, dashboardStore, "dashboard under f0", orgID, f0.ID, f0.UID, false) + + subfolder, err := folderServiceWithFlagOn.Create(context.Background(), &folder.CreateFolderCommand{ + OrgID: orgID, + Title: "subfolder", + ParentUID: f0.UID, + SignedInUser: user, + }) + require.NoError(t, err) + insertTestDashboard(t, dashboardStore, "dashboard under subfolder", orgID, subfolder.ID, subfolder.UID, false) + + type res struct { + title string + folderUID string + folderTitle string + } + + testCases := []struct { + desc string + title string + expectedResult res + typ string + }{ + { + desc: "find dashboard under general", + title: "dashboard under general", + expectedResult: res{title: "dashboard under general"}, + }, + { + desc: "find dashboard under f0", + title: "dashboard under f0", + expectedResult: res{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}, + }, + { + desc: "find dashboard under subfolder", + title: "dashboard under subfolder", + expectedResult: res{title: "dashboard under subfolder", folderUID: subfolder.UID, folderTitle: subfolder.Title}, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore, cfg), quotaService) + require.NoError(t, err) + res, err := dashboardStore.FindDashboards(context.Background(), &dashboards.FindPersistedDashboardsQuery{ + SignedInUser: user, + Type: tc.typ, + Title: tc.title, + }) + require.NoError(t, err) + require.Equal(t, 1, len(res)) + + r := tc.expectedResult + assert.Equal(t, r.title, res[0].Title) + if r.folderUID != "" { + assert.Equal(t, r.folderUID, res[0].FolderUID) + } else { + assert.Empty(t, res[0].FolderUID) + } + if r.folderTitle != "" { + assert.Equal(t, r.folderTitle, res[0].FolderTitle) + } else { + assert.Empty(t, res[0].FolderTitle) + } + }) + } +} + func TestIntegrationFindDashboardsByFolder(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") @@ -711,99 +833,240 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) { cfg := setting.NewCfg() cfg.IsFeatureToggleEnabled = func(key string) bool { return false } quotaService := quotatest.New(false, nil) - dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg), quotaService) + features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPanelTitleSearch) + dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) orgID := int64(1) - insertTestDashboard(t, dashboardStore, "dashboard under general", orgID, 0, false) + insertTestDashboard(t, dashboardStore, "dashboard under general", orgID, 0, "", false) - f0 := insertTestDashboard(t, dashboardStore, "f0", orgID, 0, true) - insertTestDashboard(t, dashboardStore, "dashboard under f0", orgID, f0.ID, false) + ac := acimpl.ProvideAccessControl(sqlStore.Cfg) + folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) + folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, folderStore, sqlStore, features) - f1 := insertTestDashboard(t, dashboardStore, "f1", orgID, 0, true) - insertTestDashboard(t, dashboardStore, "dashboard under f1", orgID, f1.ID, false) + user := &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{ + orgID: { + dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll}, + dashboards.ActionFoldersRead: []string{dashboards.ScopeFoldersAll}, + dashboards.ActionFoldersWrite: []string{dashboards.ScopeFoldersAll}, + }, + }, + } + + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanSaveValue: true, + CanViewValue: true, + // CanEditValue is required to create library elements + CanEditValue: true, + }) + t.Cleanup(func() { + guardian.New = origNewGuardian + }) + + f0, err := folderServiceWithFlagOn.Create(context.Background(), &folder.CreateFolderCommand{ + OrgID: orgID, + Title: "f0", + SignedInUser: user, + }) + require.NoError(t, err) + insertTestDashboard(t, dashboardStore, "dashboard under f0", orgID, f0.ID, f0.UID, false) + + f1, err := folderServiceWithFlagOn.Create(context.Background(), &folder.CreateFolderCommand{ + OrgID: orgID, + Title: "f1", + SignedInUser: user, + }) + require.NoError(t, err) + insertTestDashboard(t, dashboardStore, "dashboard under f1", orgID, f1.ID, f1.UID, false) + + subfolder, err := folderServiceWithFlagOn.Create(context.Background(), &folder.CreateFolderCommand{ + OrgID: orgID, + Title: "subfolder", + ParentUID: f0.UID, + SignedInUser: user, + }) + require.NoError(t, err) + + type res struct { + title string + folderUID string + folderTitle string + } testCases := []struct { desc string folderIDs []int64 folderUIDs []string - expectedResult []string + query string + expectedResult map[string][]res + typ string }{ { - desc: "find dashboard under general using folder id", - folderIDs: []int64{0}, - expectedResult: []string{"dashboard under general"}, + desc: "find dashboard under general using folder id", + folderIDs: []int64{0}, + typ: searchstore.TypeDashboard, + expectedResult: map[string][]res{ + "": {{title: "dashboard under general"}}, + featuremgmt.FlagNestedFolders: {{title: "dashboard under general"}}, + }, }, { - desc: "find dashboard under f0 using folder id", - folderIDs: []int64{f0.ID}, - expectedResult: []string{"dashboard under f0"}, + desc: "find dashboard under general using folder id", + folderIDs: []int64{0}, + typ: searchstore.TypeDashboard, + expectedResult: map[string][]res{ + "": {{title: "dashboard under general"}}, + featuremgmt.FlagNestedFolders: {{title: "dashboard under general"}}, + }, }, { - desc: "find dashboard under f0 or f1 using folder id", - folderIDs: []int64{f0.ID, f1.ID}, - expectedResult: []string{"dashboard under f0", "dashboard under f1"}, + desc: "find dashboard under f0 using folder id", + folderIDs: []int64{f0.ID}, + typ: searchstore.TypeDashboard, + expectedResult: map[string][]res{ + "": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}}, + }, }, { - desc: "find dashboard under general using folder UID", - folderUIDs: []string{folder.GeneralFolderUID}, - expectedResult: []string{"dashboard under general"}, + desc: "find dashboard under f0 or f1 using folder id", + folderIDs: []int64{f0.ID, f1.ID}, + typ: searchstore.TypeDashboard, + expectedResult: map[string][]res{ + "": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}, + {title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title}}, + featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}, + {title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title}}, + }, }, { - desc: "find dashboard under f0 using folder UID", - folderUIDs: []string{f0.UID}, - expectedResult: []string{"dashboard under f0"}, + desc: "find dashboard under general using folder UID", + folderUIDs: []string{folder.GeneralFolderUID}, + typ: searchstore.TypeDashboard, + expectedResult: map[string][]res{ + "": {{title: "dashboard under general"}}, + featuremgmt.FlagNestedFolders: {{title: "dashboard under general"}}, + }, }, { - desc: "find dashboard under f0 or f1 using folder UID", - folderUIDs: []string{f0.UID, f1.UID}, - expectedResult: []string{"dashboard under f0", "dashboard under f1"}, + desc: "find dashboard under general using folder UID", + folderUIDs: []string{folder.GeneralFolderUID}, + typ: searchstore.TypeDashboard, + expectedResult: map[string][]res{ + "": {{title: "dashboard under general"}}, + featuremgmt.FlagNestedFolders: {{title: "dashboard under general"}}, + }, }, { - desc: "find dashboard under general or f0 using folder id", - folderIDs: []int64{0, f0.ID}, - expectedResult: []string{"dashboard under f0", "dashboard under general"}, + desc: "find dashboard under f0 using folder UID", + folderUIDs: []string{f0.UID}, + typ: searchstore.TypeDashboard, + expectedResult: map[string][]res{ + "": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}}, + featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}}, + }, }, { - desc: "find dashboard under general or f0 or f1 using folder id", - folderIDs: []int64{0, f0.ID, f1.ID}, - expectedResult: []string{"dashboard under f0", "dashboard under f1", "dashboard under general"}, + desc: "find dashboard under f0 or f1 using folder UID", + folderUIDs: []string{f0.UID, f1.UID}, + typ: searchstore.TypeDashboard, + expectedResult: map[string][]res{ + "": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}, + {title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title}}, + featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}, + {title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title}}, + }, }, { - desc: "find dashboard under general or f0 using folder UID", - folderUIDs: []string{folder.GeneralFolderUID, f0.UID}, - expectedResult: []string{"dashboard under f0", "dashboard under general"}, + desc: "find dashboard under general or f0 using folder id", + folderIDs: []int64{0, f0.ID}, + typ: searchstore.TypeDashboard, + expectedResult: map[string][]res{ + "": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}, + {title: "dashboard under general"}}, + featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}, + {title: "dashboard under general"}}, + }, }, { - desc: "find dashboard under general or f0 or f1 using folder UID", - folderUIDs: []string{folder.GeneralFolderUID, f0.UID, f1.UID}, - expectedResult: []string{"dashboard under f0", "dashboard under f1", "dashboard under general"}, + desc: "find dashboard under general or f0 or f1 using folder id", + folderIDs: []int64{0, f0.ID, f1.ID}, + typ: searchstore.TypeDashboard, + expectedResult: map[string][]res{ + "": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}, + {title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title}, + {title: "dashboard under general"}}, + featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}, + {title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title}, + {title: "dashboard under general"}}, + }, + }, + { + desc: "find dashboard under general or f0 using folder UID", + folderUIDs: []string{folder.GeneralFolderUID, f0.UID}, + typ: searchstore.TypeDashboard, + expectedResult: map[string][]res{ + "": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}, + {title: "dashboard under general"}}, + featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}, + {title: "dashboard under general"}}, + }, + }, + { + desc: "find dashboard under general or f0 or f1 using folder UID", + folderUIDs: []string{folder.GeneralFolderUID, f0.UID, f1.UID}, + typ: searchstore.TypeDashboard, + expectedResult: map[string][]res{ + "": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}, + {title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title}, + {title: "dashboard under general"}}, + featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}, + {title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title}, + {title: "dashboard under general"}}, + }, + }, + { + desc: "find subfolder", + folderUIDs: []string{f0.UID}, + typ: searchstore.TypeFolder, + expectedResult: map[string][]res{ + "": {}, + featuremgmt.FlagNestedFolders: {{title: subfolder.Title, folderUID: f0.UID, folderTitle: f0.Title}}, + }, }, } for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - res, err := dashboardStore.FindDashboards(context.Background(), &dashboards.FindPersistedDashboardsQuery{ - SignedInUser: &user.SignedInUser{ - OrgID: 1, - Permissions: map[int64]map[string][]string{ - orgID: { - dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll}, - dashboards.ActionFoldersRead: []string{dashboards.ScopeFoldersAll}, - }, - }, - }, - Type: searchstore.TypeDashboard, - FolderIds: tc.folderIDs, - FolderUIDs: tc.folderUIDs, - }) - require.NoError(t, err) - require.Equal(t, len(tc.expectedResult), len(res)) + for featureFlags := range tc.expectedResult { + t.Run(fmt.Sprintf("%s with featureFlags: %v", tc.desc, featureFlags), func(t *testing.T) { + dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(featureFlags), tagimpl.ProvideService(sqlStore, cfg), quotaService) + require.NoError(t, err) + res, err := dashboardStore.FindDashboards(context.Background(), &dashboards.FindPersistedDashboardsQuery{ + SignedInUser: user, + Type: tc.typ, + FolderIds: tc.folderIDs, + FolderUIDs: tc.folderUIDs, + }) + require.NoError(t, err) + require.Equal(t, len(tc.expectedResult[featureFlags]), len(res)) - for i, r := range tc.expectedResult { - assert.Equal(t, r, res[i].Title) - } - }) + for i, r := range tc.expectedResult[featureFlags] { + assert.Equal(t, r.title, res[i].Title) + if r.folderUID != "" { + assert.Equal(t, r.folderUID, res[i].FolderUID) + } else { + assert.Empty(t, res[i].FolderUID) + } + if r.folderTitle != "" { + assert.Equal(t, r.folderTitle, res[i].FolderTitle) + } else { + assert.Empty(t, res[i].FolderTitle) + } + } + }) + } } } @@ -881,12 +1144,13 @@ func insertTestRule(t *testing.T, sqlStore db.DB, foderOrgID int64, folderUID st } func insertTestDashboard(t *testing.T, dashboardStore dashboards.Store, title string, orgId int64, - folderId int64, isFolder bool, tags ...interface{}) *dashboards.Dashboard { + folderId int64, folderUID string, isFolder bool, tags ...interface{}) *dashboards.Dashboard { t.Helper() cmd := dashboards.SaveDashboardCommand{ - OrgID: orgId, - FolderID: folderId, - IsFolder: isFolder, + OrgID: orgId, + FolderID: folderId, + FolderUID: folderUID, + IsFolder: isFolder, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": title, diff --git a/pkg/services/dashboards/database/migrations/folder_uid_migrator.go b/pkg/services/dashboards/database/migrations/folder_uid_migrator.go new file mode 100644 index 00000000000..672279e15a4 --- /dev/null +++ b/pkg/services/dashboards/database/migrations/folder_uid_migrator.go @@ -0,0 +1,81 @@ +package migrations + +import ( + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "xorm.io/xorm" +) + +// FolderUIDMigration is a code migration that populates folder_uid column +type FolderUIDMigration struct { + migrator.MigrationBase +} + +func (m *FolderUIDMigration) SQL(dialect migrator.Dialect) string { + return "code migration" +} + +func (m *FolderUIDMigration) Exec(sess *xorm.Session, mgrtr *migrator.Migrator) error { + // for dashboards the source of truth is the dashboard table + q := `UPDATE dashboard + SET folder_uid = folder.uid + FROM dashboard folder + WHERE dashboard.folder_id = folder.id + AND dashboard.is_folder = ?` + if mgrtr.Dialect.DriverName() == migrator.MySQL { + q = `UPDATE dashboard AS d + LEFT JOIN dashboard AS folder ON d.folder_id = folder.id + SET d.folder_uid = folder.uid + WHERE d.is_folder = ?` + } + + r, err := sess.Exec(q, mgrtr.Dialect.BooleanStr(false)) + if err != nil { + mgrtr.Logger.Error("Failed to migrate dashboard folder_uid for dashboards", "error", err) + return err + } + dashboardRowsAffected, dashboardRowsAffectedErr := r.RowsAffected() + if dashboardRowsAffectedErr != nil { + mgrtr.Logger.Error("Failed to get dashboard rows affected", "error", dashboardRowsAffectedErr) + } + + // for folders the source of truth is the folder table + q = `UPDATE dashboard + SET folder_uid = folder.parent_uid + FROM folder + WHERE dashboard.uid = folder.uid AND dashboard.org_id = folder.org_id + AND dashboard.is_folder = ?` + if mgrtr.Dialect.DriverName() == migrator.MySQL { + q = `UPDATE dashboard + SET folder_uid = ( + SELECT folder.parent_uid + FROM folder + WHERE dashboard.uid = folder.uid AND dashboard.org_id = folder.org_id + ) + WHERE is_folder = ?` + } + r, err = sess.Exec(q, mgrtr.Dialect.BooleanStr(true)) + if err != nil { + mgrtr.Logger.Error("Failed to migrate dashboard folder_uid for folders", "error", err) + return err + } + + folderRowsAffected, folderRowsAffectedErr := r.RowsAffected() + if folderRowsAffectedErr != nil { + mgrtr.Logger.Error("Failed to get folder rows affected", "error", folderRowsAffectedErr) + } + + mgrtr.Logger.Debug("Migrating dashboard data", "dashboards rows", dashboardRowsAffected, "folder rows", folderRowsAffected) + return nil +} + +func AddDashboardFolderMigrations(mg *migrator.Migrator) { + mg.AddMigration("Add folder_uid for dashboard", migrator.NewAddColumnMigration(migrator.Table{Name: "dashboard"}, &migrator.Column{ + Name: "folder_uid", Type: migrator.DB_NVarchar, Length: 40, Nullable: true, + })) + + mg.AddMigration("Populate dashboard folder_uid column", &FolderUIDMigration{}) + + mg.AddMigration("Add unique index for dashboard_org_id_folder_uid_title", migrator.NewAddIndexMigration(migrator.Table{Name: "dashboard"}, &migrator.Index{ + Cols: []string{"org_id", "folder_uid", "title"}, Type: migrator.UniqueIndex, + })) +} diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index 251da180073..1a572ca44c1 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -41,7 +41,8 @@ type Dashboard struct { UpdatedBy int64 CreatedBy int64 - FolderID int64 `xorm:"folder_id"` + FolderID int64 `xorm:"folder_id"` + FolderUID string `xorm:"folder_uid"` IsFolder bool HasACL bool `xorm:"has_acl"` @@ -186,6 +187,7 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { dash.PluginID = cmd.PluginID dash.IsFolder = cmd.IsFolder dash.FolderID = cmd.FolderID + dash.FolderUID = cmd.FolderUID dash.UpdateSlug() return dash } @@ -246,9 +248,10 @@ type SaveDashboardCommand struct { OrgID int64 `json:"-" xorm:"org_id"` RestoredFrom int `json:"-"` PluginID string `json:"-" xorm:"plugin_id"` - FolderID int64 `json:"folderId" xorm:"folder_id"` - FolderUID string `json:"folderUid" xorm:"folder_uid"` - IsFolder bool `json:"isFolder"` + // Deprecated: use FolderUID instead + FolderID int64 `json:"folderId" xorm:"folder_id"` + FolderUID string `json:"folderUid" xorm:"folder_uid"` + IsFolder bool `json:"isFolder"` UpdatedAt time.Time } diff --git a/pkg/services/dashboards/models_test.go b/pkg/services/dashboards/models_test.go index dcbc4102956..32f038d4315 100644 --- a/pkg/services/dashboards/models_test.go +++ b/pkg/services/dashboards/models_test.go @@ -66,7 +66,7 @@ func TestSaveDashboardCommand_GetDashboardModel(t *testing.T) { json := simplejson.New() json.Set("title", "test dash") - cmd := &SaveDashboardCommand{Dashboard: json, FolderID: 1} + cmd := &SaveDashboardCommand{Dashboard: json, FolderID: 1, FolderUID: "1"} dash := cmd.GetDashboardModel() assert.Equal(t, int64(1), dash.FolderID) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index aa041d7d315..66396ad25b9 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -92,6 +92,7 @@ func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(ctx co return dr.dashboardStore.GetProvisionedDataByDashboardUID(ctx, orgID, dashboardUID) } +//nolint:gocyclo func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, dto *dashboards.SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*dashboards.SaveDashboardCommand, error) { dash := dto.Dashboard @@ -193,6 +194,7 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d Overwrite: dto.Overwrite, UserID: userID, FolderID: dash.FolderID, + FolderUID: dash.FolderUID, IsFolder: dash.IsFolder, PluginID: dash.PluginID, } diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index 246877a9b0b..e6b33c28f4a 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -129,6 +129,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": "Dash", }), FolderID: sc.otherSavedFolder.ID, + FolderUID: sc.otherSavedFolder.UID, UserID: 10000, Overwrite: true, } @@ -152,6 +153,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": sc.savedDashInFolder.Title, }), FolderID: sc.savedFolder.ID, + FolderUID: sc.savedFolder.UID, UserID: 10000, Overwrite: true, } @@ -176,6 +178,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": "New dash", }), FolderID: sc.savedFolder.ID, + FolderUID: sc.savedFolder.UID, UserID: 10000, Overwrite: true, } @@ -200,6 +203,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": "Dash", }), FolderID: sc.savedDashInGeneralFolder.FolderID, + FolderUID: sc.savedDashInGeneralFolder.FolderUID, UserID: 10000, Overwrite: true, } @@ -224,6 +228,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": "Dash", }), FolderID: sc.savedDashInFolder.FolderID, + FolderUID: sc.savedDashInFolder.FolderUID, UserID: 10000, Overwrite: true, } @@ -248,6 +253,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": "Dash", }), FolderID: sc.otherSavedFolder.ID, + FolderUID: sc.otherSavedFolder.UID, UserID: 10000, Overwrite: true, } @@ -272,6 +278,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": "Dash", }), FolderID: 0, + FolderUID: "", UserID: 10000, Overwrite: true, } @@ -296,6 +303,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": "Dash", }), FolderID: sc.otherSavedFolder.ID, + FolderUID: sc.otherSavedFolder.UID, UserID: 10000, Overwrite: true, } @@ -320,6 +328,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": "Dash", }), FolderID: 0, + FolderUID: "", UserID: 10000, Overwrite: true, } @@ -351,6 +360,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": sc.savedDashInFolder.Title, }), FolderID: 0, + FolderUID: "", Overwrite: shouldOverwrite, } @@ -374,6 +384,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": sc.savedDashInGeneralFolder.Title, }), FolderID: sc.savedFolder.ID, + FolderUID: sc.savedFolder.UID, Overwrite: shouldOverwrite, } @@ -465,6 +476,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": "Expect error", }), FolderID: 123412321, + FolderUID: "123412321", Overwrite: shouldOverwrite, } @@ -481,6 +493,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": "test dash 23", }), FolderID: sc.savedFolder.ID, + FolderUID: sc.savedFolder.UID, Overwrite: shouldOverwrite, } @@ -498,6 +511,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "version": sc.savedDashInGeneralFolder.Version, }), FolderID: sc.savedFolder.ID, + FolderUID: sc.savedFolder.UID, Overwrite: shouldOverwrite, } @@ -521,6 +535,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": "test dash 23", }), FolderID: 0, + FolderUID: "", Overwrite: shouldOverwrite, } @@ -538,6 +553,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "version": sc.savedDashInFolder.Version, }), FolderID: 0, + FolderUID: "", Overwrite: shouldOverwrite, } @@ -560,6 +576,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": sc.savedDashInFolder.Title, }), FolderID: sc.savedDashInFolder.FolderID, + FolderUID: sc.savedDashInFolder.FolderUID, Overwrite: shouldOverwrite, } @@ -576,6 +593,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": sc.savedDashInGeneralFolder.Title, }), FolderID: sc.savedDashInGeneralFolder.FolderID, + FolderUID: sc.savedDashInGeneralFolder.FolderUID, Overwrite: shouldOverwrite, } @@ -612,6 +630,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": "Updated title", }), FolderID: sc.savedFolder.ID, + FolderUID: sc.savedFolder.UID, Overwrite: shouldOverwrite, } @@ -634,6 +653,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": "Updated title", }), FolderID: 0, + FolderUID: "", Overwrite: shouldOverwrite, } @@ -696,6 +716,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": sc.savedDashInFolder.Title, }), FolderID: sc.savedDashInFolder.FolderID, + FolderUID: sc.savedDashInFolder.FolderUID, Overwrite: shouldOverwrite, } @@ -720,6 +741,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { "title": sc.savedDashInGeneralFolder.Title, }), FolderID: sc.savedDashInGeneralFolder.FolderID, + FolderUID: sc.savedDashInGeneralFolder.FolderUID, Overwrite: shouldOverwrite, } @@ -876,9 +898,9 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc guardian.InitAccessControlGuardian(cfg, ac, dashboardService) savedFolder := saveTestFolder(t, "Saved folder", testOrgID, sqlStore) - savedDashInFolder := saveTestDashboard(t, "Saved dash in folder", testOrgID, savedFolder.ID, sqlStore) - saveTestDashboard(t, "Other saved dash in folder", testOrgID, savedFolder.ID, sqlStore) - savedDashInGeneralFolder := saveTestDashboard(t, "Saved dashboard in general folder", testOrgID, 0, sqlStore) + savedDashInFolder := saveTestDashboard(t, "Saved dash in folder", testOrgID, savedFolder.ID, savedFolder.UID, sqlStore) + saveTestDashboard(t, "Other saved dash in folder", testOrgID, savedFolder.ID, savedFolder.UID, sqlStore) + savedDashInGeneralFolder := saveTestDashboard(t, "Saved dashboard in general folder", testOrgID, 0, "", sqlStore) otherSavedFolder := saveTestFolder(t, "Other saved folder", testOrgID, sqlStore) require.Equal(t, "Saved folder", savedFolder.Title) @@ -966,13 +988,14 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto return err } -func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlStore db.DB) *dashboards.Dashboard { +func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, folderUID string, sqlStore db.DB) *dashboards.Dashboard { t.Helper() cmd := dashboards.SaveDashboardCommand{ - OrgID: orgID, - FolderID: folderID, - IsFolder: false, + OrgID: orgID, + FolderID: folderID, + FolderUID: folderUID, + IsFolder: false, Dashboard: simplejson.NewFromAny(map[string]any{ "id": nil, "title": title, @@ -1014,9 +1037,10 @@ func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlSto func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *dashboards.Dashboard { t.Helper() cmd := dashboards.SaveDashboardCommand{ - OrgID: orgID, - FolderID: 0, - IsFolder: true, + OrgID: orgID, + FolderID: 0, + FolderUID: "", + IsFolder: true, Dashboard: simplejson.NewFromAny(map[string]any{ "id": nil, "title": title, diff --git a/pkg/services/dashboardversion/dashverimpl/store_test.go b/pkg/services/dashboardversion/dashverimpl/store_test.go index 49d3d9280df..cd7d6c029c8 100644 --- a/pkg/services/dashboardversion/dashverimpl/store_test.go +++ b/pkg/services/dashboardversion/dashverimpl/store_test.go @@ -2,6 +2,7 @@ package dashverimpl import ( "context" + "strconv" "testing" "time" @@ -24,7 +25,7 @@ func testIntegrationGetDashboardVersion(t *testing.T, fn getStore) { dashVerStore := fn(ss) t.Run("Get a Dashboard ID and version ID", func(t *testing.T) { - savedDash := insertTestDashboard(t, ss, "test dash 26", 1, 0, false, "diff") + savedDash := insertTestDashboard(t, ss, "test dash 26", 1, 0, "", false, "diff") query := dashver.GetDashboardVersionQuery{ DashboardID: savedDash.ID, @@ -66,7 +67,7 @@ func testIntegrationGetDashboardVersion(t *testing.T, fn getStore) { t.Run("Clean up old dashboard versions", func(t *testing.T) { versionsToWrite := 10 for i := 0; i < versionsToWrite-1; i++ { - insertTestDashboard(t, ss, "test dash 53", 1, int64(i), false, "diff-all") + insertTestDashboard(t, ss, "test dash 53", 1, int64(i), strconv.Itoa(i), false, "diff-all") } versionIDsToDelete := []any{1, 2, 3, 4} res, err := dashVerStore.DeleteBatch( @@ -78,7 +79,7 @@ func testIntegrationGetDashboardVersion(t *testing.T, fn getStore) { assert.EqualValues(t, 4, res) }) - savedDash := insertTestDashboard(t, ss, "test dash 43", 1, 0, false, "diff-all") + savedDash := insertTestDashboard(t, ss, "test dash 43", 1, 0, "", false, "diff-all") t.Run("Get all versions for a given Dashboard ID", func(t *testing.T) { query := dashver.ListDashboardVersionsQuery{ DashboardID: savedDash.ID, @@ -134,12 +135,13 @@ var ( ) func insertTestDashboard(t *testing.T, sqlStore db.DB, title string, orgId int64, - folderId int64, isFolder bool, tags ...any) *dashboards.Dashboard { + folderId int64, folderUID string, isFolder bool, tags ...any) *dashboards.Dashboard { t.Helper() cmd := dashboards.SaveDashboardCommand{ - OrgID: orgId, - FolderID: folderId, - IsFolder: isFolder, + OrgID: orgId, + FolderID: folderId, + FolderUID: folderUID, + IsFolder: isFolder, Dashboard: simplejson.NewFromAny(map[string]any{ "id": nil, "title": title, diff --git a/pkg/services/folder/folderimpl/dashboard_folder_store_test.go b/pkg/services/folder/folderimpl/dashboard_folder_store_test.go index 5ca065ddcf2..fa8c31084dd 100644 --- a/pkg/services/folder/folderimpl/dashboard_folder_store_test.go +++ b/pkg/services/folder/folderimpl/dashboard_folder_store_test.go @@ -37,7 +37,7 @@ func TestIntegrationDashboardFolderStore(t *testing.T) { sqlStore = db.InitTestDB(t) folderStore := ProvideDashboardFolderStore(sqlStore) folder2 = insertTestFolder(t, dashboardStore, "TEST", orgId, 0, "prod") - _ = insertTestDashboard(t, dashboardStore, title, orgId, folder2.ID, "prod") + _ = insertTestDashboard(t, dashboardStore, title, orgId, folder2.ID, folder2.UID, "prod") folder1 = insertTestFolder(t, dashboardStore, title, orgId, 0, "prod") t.Run("GetFolderByTitle should find the folder", func(t *testing.T) { @@ -52,7 +52,7 @@ func TestIntegrationDashboardFolderStore(t *testing.T) { sqlStore := db.InitTestDB(t) folderStore := ProvideDashboardFolderStore(sqlStore) folder := insertTestFolder(t, dashboardStore, "TEST", orgId, 0, "prod") - dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.ID, "prod") + dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.ID, folder.UID, "prod") t.Run("should return folder by UID", func(t *testing.T) { d, err := folderStore.GetFolderByUID(context.Background(), orgId, folder.UID) @@ -76,7 +76,7 @@ func TestIntegrationDashboardFolderStore(t *testing.T) { sqlStore := db.InitTestDB(t) folderStore := ProvideDashboardFolderStore(sqlStore) folder := insertTestFolder(t, dashboardStore, "TEST", orgId, 0, "prod") - dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.ID, "prod") + dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.ID, folder.UID, "prod") t.Run("should return folder by ID", func(t *testing.T) { d, err := folderStore.GetFolderByID(context.Background(), orgId, folder.ID) @@ -96,12 +96,13 @@ func TestIntegrationDashboardFolderStore(t *testing.T) { }) } -func insertTestDashboard(t *testing.T, dashboardStore dashboards.Store, title string, orgId int64, folderId int64, tags ...any) *dashboards.Dashboard { +func insertTestDashboard(t *testing.T, dashboardStore dashboards.Store, title string, orgId int64, folderID int64, folderUID string, tags ...any) *dashboards.Dashboard { t.Helper() cmd := dashboards.SaveDashboardCommand{ - OrgID: orgId, - FolderID: folderId, - IsFolder: false, + OrgID: orgId, + FolderID: folderID, + FolderUID: folderUID, + IsFolder: false, Dashboard: simplejson.NewFromAny(map[string]any{ "id": nil, "title": title, @@ -116,12 +117,13 @@ func insertTestDashboard(t *testing.T, dashboardStore dashboards.Store, title st return dash } -func insertTestFolder(t *testing.T, dashboardStore dashboards.Store, title string, orgId int64, folderId int64, tags ...any) *dashboards.Dashboard { +func insertTestFolder(t *testing.T, dashboardStore dashboards.Store, title string, orgId int64, folderId int64, folderUID string, tags ...any) *dashboards.Dashboard { t.Helper() cmd := dashboards.SaveDashboardCommand{ - OrgID: orgId, - FolderID: folderId, - IsFolder: true, + OrgID: orgId, + FolderID: folderId, + FolderUID: folderUID, + IsFolder: true, Dashboard: simplejson.NewFromAny(map[string]any{ "id": nil, "title": title, diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index bb1d9410342..67d38d7fa21 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -276,6 +276,9 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( return nil, folder.ErrBadRequest.Errorf("missing signed in user") } + dashFolder := dashboards.NewDashboardFolder(cmd.Title) + dashFolder.OrgID = cmd.OrgID + if s.features.IsEnabled(featuremgmt.FlagNestedFolders) && cmd.ParentUID != "" { // Check that the user is allowed to create a subfolder in this folder evaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(cmd.ParentUID)) @@ -286,11 +289,9 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( if !hasAccess { return nil, dashboards.ErrFolderAccessDenied } + dashFolder.FolderUID = cmd.ParentUID } - dashFolder := dashboards.NewDashboardFolder(cmd.Title) - dashFolder.OrgID = cmd.OrgID - trimmedUID := strings.TrimSpace(cmd.UID) if trimmedUID == accesscontrol.GeneralFolderUID { return nil, dashboards.ErrFolderInvalidUID @@ -325,7 +326,7 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( User: user, } - saveDashboardCmd, err := s.BuildSaveDashboardCommand(ctx, dto) + saveDashboardCmd, err := s.buildSaveDashboardCommand(ctx, dto) if err != nil { return nil, toFolderError(err) } @@ -419,6 +420,10 @@ func (s *Service) legacyUpdate(ctx context.Context, cmd *folder.UpdateFolderComm } dashFolder := queryResult + if cmd.NewParentUID != nil { + dashFolder.FolderUID = *cmd.NewParentUID + } + currentTitle := dashFolder.Title if !dashFolder.IsFolder { @@ -447,7 +452,7 @@ func (s *Service) legacyUpdate(ctx context.Context, cmd *folder.UpdateFolderComm Overwrite: cmd.Overwrite, } - saveDashboardCmd, err := s.BuildSaveDashboardCommand(ctx, dto) + saveDashboardCmd, err := s.buildSaveDashboardCommand(ctx, dto) if err != nil { return nil, toFolderError(err) } @@ -621,7 +626,7 @@ func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*fol // if the current folder is already a parent of newparent, we should return error for _, parent := range parents { if parent.UID == cmd.UID { - return nil, folder.ErrCircularReference + return nil, folder.ErrCircularReference.Errorf("failed to move folder") } } @@ -629,12 +634,34 @@ func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*fol if cmd.NewParentUID != "" { newParentUID = cmd.NewParentUID } - return s.store.Update(ctx, folder.UpdateFolderCommand{ - UID: cmd.UID, - OrgID: cmd.OrgID, - NewParentUID: &newParentUID, - SignedInUser: cmd.SignedInUser, - }) + + var f *folder.Folder + if err := s.db.InTransaction(ctx, func(ctx context.Context) error { + if f, err = s.store.Update(ctx, folder.UpdateFolderCommand{ + UID: cmd.UID, + OrgID: cmd.OrgID, + NewParentUID: &newParentUID, + SignedInUser: cmd.SignedInUser, + }); err != nil { + return folder.ErrInternal.Errorf("failed to move folder: %w", err) + } + + if _, err := s.legacyUpdate(ctx, &folder.UpdateFolderCommand{ + UID: cmd.UID, + OrgID: cmd.OrgID, + NewParentUID: &newParentUID, + SignedInUser: cmd.SignedInUser, + // bypass optimistic locking used for dashboards + Overwrite: true, + }); err != nil { + return folder.ErrInternal.Errorf("failed to move legacy folder: %w", err) + } + + return nil + }); err != nil { + return nil, err + } + return f, nil } // nestedFolderDelete inspects the folder referenced by the cmd argument, deletes all the entries for @@ -735,9 +762,9 @@ func (s *Service) getNestedFolders(ctx context.Context, orgID int64, uid string) return result, nil } -// BuildSaveDashboardCommand is a simplified version on DashboardServiceImpl.BuildSaveDashboardCommand +// buildSaveDashboardCommand is a simplified version on DashboardServiceImpl.buildSaveDashboardCommand // keeping only the meaningful functionality for folders -func (s *Service) BuildSaveDashboardCommand(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*dashboards.SaveDashboardCommand, error) { +func (s *Service) buildSaveDashboardCommand(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*dashboards.SaveDashboardCommand, error) { dash := dto.Dashboard dash.OrgID = dto.OrgID @@ -807,6 +834,7 @@ func (s *Service) BuildSaveDashboardCommand(ctx context.Context, dto *dashboards Overwrite: dto.Overwrite, UserID: userID, FolderID: dash.FolderID, + FolderUID: dash.FolderUID, IsFolder: dash.IsFolder, PluginID: dash.PluginID, } diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 0d2e5eaa6db..3b4fd217c92 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -416,8 +416,8 @@ func TestIntegrationNestedFolderService(t *testing.T) { require.NoError(t, err) subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[1]) require.NoError(t, err) - _ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, "prod") - _ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in subfolder", orgID, subfolder.ID, "prod") + _ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, parent.UID, "prod") + _ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in subfolder", orgID, subfolder.ID, subfolder.UID, "prod") _ = createRule(t, alertStore, parent.UID, "parent alert") _ = createRule(t, alertStore, subfolder.UID, "sub alert") @@ -491,8 +491,8 @@ func TestIntegrationNestedFolderService(t *testing.T) { require.NoError(t, err) subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[1]) require.NoError(t, err) - _ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, "prod") - _ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in subfolder", orgID, subfolder.ID, "prod") + _ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, parent.UID, "prod") + _ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in subfolder", orgID, subfolder.ID, subfolder.UID, "prod") _ = createRule(t, alertStore, parent.UID, "parent alert") _ = createRule(t, alertStore, subfolder.UID, "sub alert") @@ -996,9 +996,10 @@ func TestNestedFolderService(t *testing.T) { nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("newFolder")}} folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), acimpl.ProvideAccessControl(setting.NewCfg()), dbtest.NewFakeDB()) - f, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder", OrgID: orgID, SignedInUser: nestedFolderUser}) + _, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder", OrgID: orgID, SignedInUser: nestedFolderUser}) require.NoError(t, err) - require.NotNil(t, f) + // the folder is set inside InTransaction() but the fake one is called + // require.NotNil(t, f) }) t.Run("move to the root folder without folder creation permissions fails", func(t *testing.T) { @@ -1032,9 +1033,10 @@ func TestNestedFolderService(t *testing.T) { nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersCreate: {}} folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), acimpl.ProvideAccessControl(setting.NewCfg()), dbtest.NewFakeDB()) - f, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "", OrgID: orgID, SignedInUser: nestedFolderUser}) + _, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "", OrgID: orgID, SignedInUser: nestedFolderUser}) require.NoError(t, err) - require.NotNil(t, f) + // the folder is set inside InTransaction() but the fake one is called + // require.NotNil(t, f) }) t.Run("move when parentUID in the current subtree returns error from nested folder service", func(t *testing.T) { diff --git a/pkg/services/publicdashboards/api/query_test.go b/pkg/services/publicdashboards/api/query_test.go index 5e10c763c25..51d823d0048 100644 --- a/pkg/services/publicdashboards/api/query_test.go +++ b/pkg/services/publicdashboards/api/query_test.go @@ -289,9 +289,10 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) // Create Dashboard saveDashboardCmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - FolderID: 1, - IsFolder: false, + OrgID: 1, + FolderID: 1, + FolderUID: "1", + IsFolder: false, Dashboard: simplejson.NewFromAny(map[string]any{ "id": nil, "title": "test", diff --git a/pkg/services/publicdashboards/database/database_test.go b/pkg/services/publicdashboards/database/database_test.go index cd3ff36daf4..2379b99a795 100644 --- a/pkg/services/publicdashboards/database/database_test.go +++ b/pkg/services/publicdashboards/database/database_test.go @@ -63,9 +63,9 @@ func TestIntegrationListPublicDashboard(t *testing.T) { require.NoError(t, err) publicdashboardStore = ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) - bDash = insertTestDashboard(t, dashboardStore, "b", orgId, 0, false) - aDash = insertTestDashboard(t, dashboardStore, "a", orgId, 0, false) - cDash = insertTestDashboard(t, dashboardStore, "c", orgId, 0, false) + bDash = insertTestDashboard(t, dashboardStore, "b", orgId, 0, "", false) + aDash = insertTestDashboard(t, dashboardStore, "a", orgId, 0, "", false) + cDash = insertTestDashboard(t, dashboardStore, "c", orgId, 0, "", false) // these are in order of how they should be returned from ListPUblicDashboards aPublicDash = insertPublicDashboard(t, publicdashboardStore, aDash.UID, orgId, false, PublicShareType) @@ -178,7 +178,7 @@ func TestIntegrationFindDashboard(t *testing.T) { require.NoError(t, err) dashboardStore = store publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) - savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true) } t.Run("FindDashboard can get original dashboard by uid", func(t *testing.T) { @@ -208,7 +208,7 @@ func TestIntegrationExistsEnabledByAccessToken(t *testing.T) { require.NoError(t, err) dashboardStore = store publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) - savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true) } t.Run("ExistsEnabledByAccessToken will return true when at least one public dashboard has a matching access token", func(t *testing.T) { setup() @@ -281,7 +281,7 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) { require.NoError(t, err) dashboardStore = store publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) - savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true) } t.Run("ExistsEnabledByDashboardUid Will return true when dashboard has at least one enabled public dashboard", func(t *testing.T) { @@ -346,7 +346,7 @@ func TestIntegrationFindByDashboardUid(t *testing.T) { require.NoError(t, err) dashboardStore = store publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) - savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true) } t.Run("returns public dashboard by dashboardUid", func(t *testing.T) { @@ -413,7 +413,7 @@ func TestIntegrationFindByAccessToken(t *testing.T) { dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotatest.New(false, nil)) require.NoError(t, err) publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) - savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true) } t.Run("returns public dashboard by accessToken", func(t *testing.T) { @@ -483,8 +483,8 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) { require.NoError(t, err) dashboardStore = store publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) - savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) - savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, true) + savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true) + savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, "", true) insertPublicDashboard(t, publicdashboardStore, savedDashboard2.UID, savedDashboard2.OrgID, false, PublicShareType) } @@ -562,8 +562,8 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) - savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) - anotherSavedDashboard = insertTestDashboard(t, dashboardStore, "test another Dashie", 1, 0, true) + savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true) + anotherSavedDashboard = insertTestDashboard(t, dashboardStore, "test another Dashie", 1, 0, "", true) } t.Run("updates an existing dashboard", func(t *testing.T) { @@ -666,7 +666,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) { dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) - savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true) } t.Run("GetOrgIdByAccessToken will OrgId when enabled", func(t *testing.T) { setup() @@ -738,7 +738,7 @@ func TestIntegrationDelete(t *testing.T) { dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotatest.New(false, nil)) require.NoError(t, err) publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) - savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true) savedPublicDashboard = insertPublicDashboard(t, publicdashboardStore, savedDashboard.UID, savedDashboard.OrgID, true, PublicShareType) } @@ -790,9 +790,9 @@ func TestGetDashboardByFolder(t *testing.T) { dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) pubdashStore := ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) - dashboard := insertTestDashboard(t, dashboardStore, "title", 1, 1, true, PublicShareType) + dashboard := insertTestDashboard(t, dashboardStore, "title", 1, 1, "1", true, PublicShareType) pubdash := insertPublicDashboard(t, pubdashStore, dashboard.UID, dashboard.OrgID, true, PublicShareType) - dashboard2 := insertTestDashboard(t, dashboardStore, "title", 1, 2, true, PublicShareType) + dashboard2 := insertTestDashboard(t, dashboardStore, "title", 1, 2, "2", true, PublicShareType) _ = insertPublicDashboard(t, pubdashStore, dashboard2.UID, dashboard2.OrgID, true, PublicShareType) pubdashes, err := pubdashStore.FindByDashboardFolder(context.Background(), dashboard) @@ -823,10 +823,10 @@ func TestGetMetrics(t *testing.T) { require.NoError(t, err) dashboardStore = store publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) - savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, false) - savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, false) - savedDashboard3 = insertTestDashboard(t, dashboardStore, "testDashie3", 2, 0, false) - savedDashboard4 = insertTestDashboard(t, dashboardStore, "testDashie4", 2, 0, false) + savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", false) + savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, "", false) + savedDashboard3 = insertTestDashboard(t, dashboardStore, "testDashie3", 2, 0, "", false) + savedDashboard4 = insertTestDashboard(t, dashboardStore, "testDashie4", 2, 0, "", false) insertPublicDashboard(t, publicdashboardStore, savedDashboard.UID, savedDashboard.OrgID, true, PublicShareType) insertPublicDashboard(t, publicdashboardStore, savedDashboard2.UID, savedDashboard2.OrgID, true, PublicShareType) insertPublicDashboard(t, publicdashboardStore, savedDashboard3.UID, savedDashboard3.OrgID, true, EmailShareType) @@ -868,12 +868,13 @@ func TestGetMetrics(t *testing.T) { // helper function to insert a dashboard func insertTestDashboard(t *testing.T, dashboardStore dashboards.Store, title string, orgId int64, - folderId int64, isFolder bool, tags ...any) *dashboards.Dashboard { + folderId int64, folderUID string, isFolder bool, tags ...any) *dashboards.Dashboard { t.Helper() cmd := dashboards.SaveDashboardCommand{ - OrgID: orgId, - FolderID: folderId, - IsFolder: isFolder, + OrgID: orgId, + FolderID: folderId, + FolderUID: folderUID, + IsFolder: isFolder, Dashboard: simplejson.NewFromAny(map[string]any{ "id": nil, "title": title, diff --git a/pkg/services/publicdashboards/service/query_test.go b/pkg/services/publicdashboards/service/query_test.go index 2c325919112..05ab7194513 100644 --- a/pkg/services/publicdashboards/service/query_test.go +++ b/pkg/services/publicdashboards/service/query_test.go @@ -721,7 +721,7 @@ func TestGetQueryDataResponse(t *testing.T) { "targets": []interface{}{hiddenQuery}, }} - dashboard := insertTestDashboard(t, dashboardStore, "testDashWithHiddenQuery", 1, 0, true, []map[string]interface{}{}, customPanels) + dashboard := insertTestDashboard(t, dashboardStore, "testDashWithHiddenQuery", 1, 0, "", true, []map[string]interface{}{}, customPanels) isEnabled := true dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, @@ -1130,7 +1130,7 @@ func TestGetMetricRequest(t *testing.T) { dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil)) require.NoError(t, err) publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]interface{}{}, nil) publicDashboard := &PublicDashboard{ Uid: "1", DashboardUid: dashboard.UID, @@ -1216,8 +1216,8 @@ func TestBuildMetricRequest(t *testing.T) { require.NoError(t, err) publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) - publicDashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) - nonPublicDashboard := insertTestDashboard(t, dashboardStore, "testNonPublicDashie", 1, 0, true, []map[string]interface{}{}, nil) + publicDashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]interface{}{}, nil) + nonPublicDashboard := insertTestDashboard(t, dashboardStore, "testNonPublicDashie", 1, 0, "", true, []map[string]interface{}{}, nil) from, to := internal.GetTimeRangeFromDashboard(t, publicDashboard.Data) service := &PublicDashboardServiceImpl{ @@ -1343,7 +1343,7 @@ func TestBuildMetricRequest(t *testing.T) { "targets": []interface{}{hiddenQuery, nonHiddenQuery}, }} - publicDashboard := insertTestDashboard(t, dashboardStore, "testDashWithHiddenQuery", 1, 0, true, []map[string]interface{}{}, customPanels) + publicDashboard := insertTestDashboard(t, dashboardStore, "testDashWithHiddenQuery", 1, 0, "", true, []map[string]interface{}{}, customPanels) reqDTO, err := service.buildMetricRequest( publicDashboard, @@ -1375,7 +1375,7 @@ func TestBuildAnonymousUser(t *testing.T) { sqlStore := db.InitTestDB(t) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil)) require.NoError(t, err) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]interface{}{}, nil) t.Run("will add datasource read and query permissions to user for each datasource in dashboard", func(t *testing.T) { user := buildAnonymousUser(context.Background(), dashboard) diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index 4a85e60c020..2643d3aca93 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -594,7 +594,7 @@ func TestCreatePublicDashboard(t *testing.T) { dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]any{}, nil) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) service := &PublicDashboardServiceImpl{ @@ -680,7 +680,7 @@ func TestCreatePublicDashboard(t *testing.T) { dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]any{}, nil) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) service := &PublicDashboardServiceImpl{ @@ -718,7 +718,7 @@ func TestCreatePublicDashboard(t *testing.T) { dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]any{}, nil) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) service := &PublicDashboardServiceImpl{ @@ -752,7 +752,7 @@ func TestCreatePublicDashboard(t *testing.T) { require.NoError(t, err) publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) templateVars := make([]map[string]any, 1) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, templateVars, nil) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, templateVars, nil) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) service := &PublicDashboardServiceImpl{ @@ -827,7 +827,7 @@ func TestCreatePublicDashboard(t *testing.T) { dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]any{}, nil) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) service := &PublicDashboardServiceImpl{ @@ -905,7 +905,7 @@ func TestCreatePublicDashboard(t *testing.T) { dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]interface{}{}, nil) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) service := &PublicDashboardServiceImpl{ @@ -966,7 +966,7 @@ func TestCreatePublicDashboard(t *testing.T) { dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil)) require.NoError(t, err) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]any{}, nil) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) publicdashboardStore := &FakePublicDashboardStore{} publicdashboardStore.On("FindByDashboardUid", mock.Anything, mock.Anything, mock.Anything).Return(&PublicDashboard{Uid: "newPubdashUid"}, nil) @@ -1003,7 +1003,7 @@ func TestCreatePublicDashboard(t *testing.T) { dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]any{}, nil) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) service := &PublicDashboardServiceImpl{ @@ -1047,8 +1047,8 @@ func TestUpdatePublicDashboard(t *testing.T) { require.NoError(t, err) publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]any{}, nil) - dashboard2 := insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, true, []map[string]any{}, nil) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) + dashboard2 := insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, "", true, []map[string]any{}, nil) service := &PublicDashboardServiceImpl{ log: log.New("test.logger"), @@ -1233,7 +1233,7 @@ func TestUpdatePublicDashboard(t *testing.T) { require.NoError(t, err) publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]any{}, nil) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) service := &PublicDashboardServiceImpl{ log: log.New("test.logger"), @@ -1803,7 +1803,7 @@ func AddAnnotationsToDashboard(t *testing.T, dash *dashboards.Dashboard, annotat } func insertTestDashboard(t *testing.T, dashboardStore dashboards.Store, title string, orgId int64, - folderId int64, isFolder bool, templateVars []map[string]any, customPanels []any, tags ...any) *dashboards.Dashboard { + folderId int64, folderUID string, isFolder bool, templateVars []map[string]any, customPanels []any, tags ...any) *dashboards.Dashboard { t.Helper() var dashboardPanels []any @@ -1852,9 +1852,10 @@ func insertTestDashboard(t *testing.T, dashboardStore dashboards.Store, title st } cmd := dashboards.SaveDashboardCommand{ - OrgID: orgId, - FolderID: folderId, - IsFolder: isFolder, + OrgID: orgId, + FolderID: folderId, + FolderUID: folderUID, + IsFolder: isFolder, Dashboard: simplejson.NewFromAny(map[string]any{ "id": nil, "title": title, diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 414433f4c71..e16117c5df5 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -1,6 +1,7 @@ package migrations import ( + dashboardFolderMigrations "github.com/grafana/grafana/pkg/services/dashboards/database/migrations" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore/migrations/anonservice" @@ -103,6 +104,8 @@ func (*OSSMigrations) AddMigration(mg *Migrator) { ualert.MigrationServiceMigration(mg) ualert.CreatedFoldersMigration(mg) + + dashboardFolderMigrations.AddDashboardFolderMigrations(mg) } func addStarMigrations(mg *Migrator) { diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index 2862cbbdc58..e960be75715 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -815,8 +815,9 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol // create dashboard under parent folder _, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{ - OrgID: orgID, - FolderID: parent.ID, + OrgID: orgID, + FolderID: parent.ID, + FolderUID: parent.UID, Dashboard: simplejson.NewFromAny(map[string]any{ "title": "dashboard under parent folder", }), @@ -825,8 +826,9 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol // create dashboard under subfolder _, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{ - OrgID: orgID, - FolderID: subfolder.ID, + OrgID: orgID, + FolderID: subfolder.ID, + FolderUID: subfolder.UID, Dashboard: simplejson.NewFromAny(map[string]any{ "title": "dashboard under subfolder", }), diff --git a/pkg/services/sqlstore/searchstore/builder.go b/pkg/services/sqlstore/searchstore/builder.go index cdfd3efbf99..38c4ec0fa94 100644 --- a/pkg/services/sqlstore/searchstore/builder.go +++ b/pkg/services/sqlstore/searchstore/builder.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) @@ -14,8 +15,9 @@ import ( type Builder struct { // List of FilterWhere/FilterGroupBy/FilterOrderBy/FilterLeftJoin // to modify the query. - Filters []any - Dialect migrator.Dialect + Filters []any + Dialect migrator.Dialect + Features featuremgmt.FeatureToggles params []any sql bytes.Buffer @@ -35,9 +37,15 @@ func (b *Builder) ToSQL(limit, page int64) (string, []any) { INNER JOIN dashboard ON ids.id = dashboard.id`) b.sql.WriteString("\n") - b.sql.WriteString( - `LEFT OUTER JOIN dashboard AS folder ON folder.id = dashboard.folder_id - LEFT OUTER JOIN dashboard_tag ON dashboard.id = dashboard_tag.dashboard_id`) + if b.Features.IsEnabled(featuremgmt.FlagNestedFolders) { + b.sql.WriteString( + `LEFT OUTER JOIN folder ON folder.uid = dashboard.folder_uid AND folder.org_id = dashboard.org_id`) + } else { + b.sql.WriteString(` + LEFT OUTER JOIN dashboard AS folder ON folder.id = dashboard.folder_id`) + } + b.sql.WriteString(` + LEFT OUTER JOIN dashboard_tag ON dashboard.id = dashboard_tag.dashboard_id`) b.sql.WriteString("\n") b.sql.WriteString(orderQuery) @@ -58,7 +66,15 @@ func (b *Builder) buildSelect() { dashboard.is_folder, dashboard.folder_id, folder.uid AS folder_uid, - folder.slug AS folder_slug, + `) + if b.Features.IsEnabled(featuremgmt.FlagNestedFolders) { + b.sql.WriteString(` + folder.title AS folder_slug,`) + } else { + b.sql.WriteString(` + folder.slug AS folder_slug,`) + } + b.sql.WriteString(` folder.title AS folder_title `) for _, f := range b.Filters { diff --git a/pkg/services/sqlstore/searchstore/filters.go b/pkg/services/sqlstore/searchstore/filters.go index c17cba042ce..49cd2c1496c 100644 --- a/pkg/services/sqlstore/searchstore/filters.go +++ b/pkg/services/sqlstore/searchstore/filters.go @@ -58,9 +58,10 @@ func (f FolderFilter) Where() (string, []any) { } type FolderUIDFilter struct { - Dialect migrator.Dialect - OrgID int64 - UIDs []string + Dialect migrator.Dialect + OrgID int64 + UIDs []string + NestedFoldersEnabled bool } func (f FolderUIDFilter) Where() (string, []any) { @@ -84,10 +85,16 @@ func (f FolderUIDFilter) Where() (string, []any) { // do nothing case len(params) == 1: q = "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid = ?)" + if f.NestedFoldersEnabled { + q = "dashboard.org_id = ? AND dashboard.folder_uid = ?" + } params = append([]any{f.OrgID}, params...) default: sqlArray := "(?" + strings.Repeat(",?", len(params)-1) + ")" q = "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid IN " + sqlArray + ")" + if f.NestedFoldersEnabled { + q = "dashboard.org_id = ? AND dashboard.folder_uid IN " + sqlArray + } params = append([]any{f.OrgID}, params...) } diff --git a/pkg/services/sqlstore/searchstore/search_test.go b/pkg/services/sqlstore/searchstore/search_test.go index ca21074808c..b36b0323b0f 100644 --- a/pkg/services/sqlstore/searchstore/search_test.go +++ b/pkg/services/sqlstore/searchstore/search_test.go @@ -46,7 +46,8 @@ func TestBuilder_EqualResults_Basic(t *testing.T) { searchstore.OrgFilter{OrgId: user.OrgID}, searchstore.TitleSorter{}, }, - Dialect: store.GetDialect(), + Dialect: store.GetDialect(), + Features: featuremgmt.WithFeatures(), } res := []dashboards.DashboardSearchProjection{} @@ -83,7 +84,8 @@ func TestBuilder_Pagination(t *testing.T) { searchstore.OrgFilter{OrgId: user.OrgID}, searchstore.TitleSorter{}, }, - Dialect: store.GetDialect(), + Dialect: store.GetDialect(), + Features: featuremgmt.WithFeatures(), } resPg1 := []dashboards.DashboardSearchProjection{} @@ -243,7 +245,8 @@ func TestBuilder_RBAC(t *testing.T) { recursiveQueriesAreSupported, ), }, - Dialect: store.GetDialect(), + Dialect: store.GetDialect(), + Features: features, } res := []dashboards.DashboardSearchProjection{} From afa697f95449bdedb919572301470cd27f9ec89a Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Tue, 24 Oct 2023 09:28:23 +0200 Subject: [PATCH 003/180] Build: Faster external plugin builds (#76974) * build(plugin-configs): move swc-loader to configs and use require.resolve for less duplicate deps * build(plugins): move cacheLocation to ESLintPlugin, run lint and tscheck plugins in dev * revert(plugins): remove obsolete stats setting --- packages/grafana-plugin-configs/package.json | 1 + .../grafana-plugin-configs/webpack.config.ts | 44 ++++++++++--------- .../grafana-testdata-datasource/package.json | 1 - yarn.lock | 2 +- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index cae7f3f444b..db5e3783f72 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -13,6 +13,7 @@ "fork-ts-checker-webpack-plugin": "8.0.0", "glob": "10.3.3", "replace-in-file-webpack-plugin": "1.0.6", + "swc-loader": "0.2.3", "webpack": "5.89.0" }, "packageManager": "yarn@3.6.0" diff --git a/packages/grafana-plugin-configs/webpack.config.ts b/packages/grafana-plugin-configs/webpack.config.ts index 315f4a34f07..7d5f7032524 100644 --- a/packages/grafana-plugin-configs/webpack.config.ts +++ b/packages/grafana-plugin-configs/webpack.config.ts @@ -3,7 +3,7 @@ import ESLintPlugin from 'eslint-webpack-plugin'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; import path from 'path'; import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin'; -import { Configuration } from 'webpack'; +import { Configuration, DefinePlugin } from 'webpack'; import { DIST_DIR } from './constants'; import { getPackageJson, getPluginJson, getEntries, hasLicense } from './utils'; @@ -29,12 +29,6 @@ const config = async (env: Record): Promise => { config: [__filename], }, cacheDirectory: path.resolve(__dirname, '../../.yarn/.cache/webpack', path.basename(process.cwd())), - cacheLocation: path.resolve( - __dirname, - '../../.yarn/.cache/eslint-webpack-plugin', - path.basename(process.cwd()), - '.eslintcache' - ), }, context: process.cwd(), @@ -89,7 +83,7 @@ const config = async (env: Record): Promise => { exclude: /(node_modules)/, test: /\.[tj]sx?$/, use: { - loader: 'swc-loader', + loader: require.resolve('swc-loader'), options: { jsc: { baseUrl: '.', @@ -186,21 +180,31 @@ const config = async (env: Record): Promise => { ], }, ]), - new ForkTsCheckerWebpackPlugin({ - async: Boolean(env.development), - issue: { - include: [{ file: '**/*.{ts,tsx}' }], - }, - typescript: { configFile: path.join(process.cwd(), 'tsconfig.json') }, - }), - new ESLintPlugin({ - extensions: ['.ts', '.tsx'], - lintDirtyModulesOnly: Boolean(env.development), // don't lint on start, only lint changed files - }), + env.development + ? new ForkTsCheckerWebpackPlugin({ + async: true, + issue: { + include: [{ file: '**/*.{ts,tsx}' }], + }, + typescript: { configFile: path.join(process.cwd(), 'tsconfig.json') }, + }) + : new DefinePlugin({}), + env.development + ? new ESLintPlugin({ + extensions: ['.ts', '.tsx'], + lintDirtyModulesOnly: true, // don't lint on start, only lint changed files + cacheLocation: path.resolve( + __dirname, + '../../.yarn/.cache/eslint-webpack-plugin', + path.basename(process.cwd()), + '.eslintcache' + ), + }) + : new DefinePlugin({}), ], resolve: { - extensions: ['.js', '.jsx', '.ts', '.tsx'], + extensions: ['.ts', '.tsx', '.js', '.jsx'], unsafeCache: true, }, diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 45c8bbf60ba..3f3e7d21458 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -26,7 +26,6 @@ "@types/node": "18.18.5", "@types/react": "18.2.15", "@types/testing-library__jest-dom": "5.14.8", - "swc-loader": "0.2.3", "ts-node": "10.9.1", "webpack": "5.89.0" }, diff --git a/yarn.lock b/yarn.lock index d0a3198a58e..8405e1e71ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2857,7 +2857,6 @@ __metadata: react: 18.2.0 react-use: 17.4.0 rxjs: 7.8.1 - swc-loader: 0.2.3 ts-node: 10.9.1 tslib: 2.6.0 webpack: 5.89.0 @@ -3255,6 +3254,7 @@ __metadata: fork-ts-checker-webpack-plugin: 8.0.0 glob: 10.3.3 replace-in-file-webpack-plugin: 1.0.6 + swc-loader: 0.2.3 tslib: 2.6.0 webpack: 5.89.0 languageName: unknown From 4ea4156b089560451bd55947264ffd83fc577398 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Tue, 24 Oct 2023 10:25:23 +0200 Subject: [PATCH 004/180] Elasticsearch: decouple from timeSrv and templateSrv (#76894) * Elasticsearch: remove timeSrv dependencies * Elastisearch: decouple from templateSrv * Prettier * Elasticsearch: improve typescript in mocks file * Prettier --- .../elasticsearch/LanguageProvider.test.ts | 9 +- .../elasticsearch/datasource.test.ts | 184 ++++++------------ .../datasource/elasticsearch/datasource.ts | 41 ++-- .../plugins/datasource/elasticsearch/mocks.ts | 20 +- 4 files changed, 98 insertions(+), 156 deletions(-) diff --git a/public/app/plugins/datasource/elasticsearch/LanguageProvider.test.ts b/public/app/plugins/datasource/elasticsearch/LanguageProvider.test.ts index a183531a9a4..9292bf0e741 100644 --- a/public/app/plugins/datasource/elasticsearch/LanguageProvider.test.ts +++ b/public/app/plugins/datasource/elasticsearch/LanguageProvider.test.ts @@ -1,7 +1,5 @@ import { AbstractLabelOperator, AbstractQuery } from '@grafana/data'; -import { TemplateSrv } from '../../../features/templating/template_srv'; - import LanguageProvider from './LanguageProvider'; import { ElasticDatasource } from './datasource'; import { createElasticDatasource } from './mocks'; @@ -14,12 +12,7 @@ const baseLogsQuery: Partial = { describe('transform abstract query to elasticsearch query', () => { let datasource: ElasticDatasource; beforeEach(() => { - const templateSrvStub = { - getAdhocFilters: jest.fn(() => []), - replace: jest.fn((a: string) => a), - } as unknown as TemplateSrv; - - datasource = createElasticDatasource({}, templateSrvStub); + datasource = createElasticDatasource(); }); it('With some labels', () => { diff --git a/public/app/plugins/datasource/elasticsearch/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/datasource.test.ts index fcb59be0bce..ad7b29dd6ec 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.test.ts @@ -8,21 +8,17 @@ import { DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings, - dateMath, DateTime, dateTime, Field, FieldType, MutableDataFrame, - RawTimeRange, SupplementaryQueryType, TimeRange, toUtc, } from '@grafana/data'; import { BackendSrvRequest, FetchResponse, reportInteraction, config } from '@grafana/runtime'; import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ -import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; -import { TemplateSrv } from 'app/features/templating/template_srv'; import { createFetchResponse } from '../../../../test/helpers/createFetchResponse'; @@ -48,8 +44,8 @@ jest.mock('@grafana/runtime', () => ({ }, })); -const TIMESRV_START = [2022, 8, 21, 6, 10, 10]; -const TIMESRV_END = [2022, 8, 24, 6, 10, 21]; +const TIME_START = [2022, 8, 21, 6, 10, 10]; +const TIME_END = [2022, 8, 24, 6, 10, 21]; const DATAQUERY_BASE = { requestId: '1', interval: '', @@ -62,13 +58,6 @@ const DATAQUERY_BASE = { startTime: 0, }; -jest.mock('app/features/dashboard/services/TimeSrv', () => ({ - ...jest.requireActual('app/features/dashboard/services/TimeSrv'), - getTimeSrv: () => ({ - timeRange: () => createTimeRange(toUtc(TIMESRV_START), toUtc(TIMESRV_END)), - }), -})); - const createTimeRange = (from: DateTime, to: DateTime): TimeRange => ({ from, to, @@ -80,60 +69,29 @@ const createTimeRange = (from: DateTime, to: DateTime): TimeRange => ({ interface TestContext { data?: Data; - from?: string; jsonData?: Partial; database?: string; fetchMockImplementation?: (options: BackendSrvRequest) => Observable; - templateSrvMock?: TemplateSrv; } interface Data { [key: string]: undefined | string | string[] | number | Data | Data[]; } -function getTestContext({ - data = { responses: [] }, - from = 'now-5m', - jsonData, - fetchMockImplementation, - templateSrvMock, -}: TestContext = {}) { +function getTestContext({ data = { responses: [] }, jsonData, fetchMockImplementation }: TestContext = {}) { const defaultMock = (options: BackendSrvRequest) => of(createFetchResponse(data)); const fetchMock = jest.spyOn(backendSrv, 'fetch'); fetchMock.mockImplementation(fetchMockImplementation ?? defaultMock); - const timeSrv = { - time: { from, to: 'now' }, - timeRange: () => ({ - from: dateMath.parse(timeSrv.time.from, false), - to: dateMath.parse(timeSrv.time.to, true), - }), - setTime: (time: RawTimeRange) => { - timeSrv.time = time; - }, - } as TimeSrv; - const settings: Partial> = { url: ELASTICSEARCH_MOCK_URL }; settings.jsonData = jsonData as ElasticsearchOptions; - const templateSrv = - templateSrvMock ?? - ({ - replace: (text?: string) => { - if (text?.startsWith('$')) { - return `resolvedVariable`; - } else { - return text; - } - }, - containsTemplate: jest.fn().mockImplementation((text?: string) => text?.includes('$') ?? false), - getAdhocFilters: jest.fn().mockReturnValue([]), - } as unknown as TemplateSrv); + const ds = createElasticDatasource(settings); - const ds = createElasticDatasource(settings, templateSrv); + const timeRange = createTimeRange(toUtc(TIME_START), toUtc(TIME_END)); - return { timeSrv, ds, fetchMock, templateSrv }; + return { ds, fetchMock, timeRange }; } describe('ElasticDatasource', () => { @@ -167,12 +125,12 @@ describe('ElasticDatasource', () => { }, ], }; - const { ds, fetchMock } = getTestContext({ + const { ds, fetchMock, timeRange } = getTestContext({ data, jsonData: { interval: 'Daily', timeField: '@timestamp' }, }); - ds.getTagValues({ key: 'test' }); + ds.getTagValues({ key: 'test', timeRange: timeRange, filters: [] }); expect(fetchMock).toHaveBeenCalledTimes(1); const obj = JSON.parse(fetchMock.mock.calls[0][0].data.split('\n')[1]); @@ -501,7 +459,6 @@ describe('ElasticDatasource', () => { const { ds } = getTestContext({ fetchMockImplementation: () => throwError(response), - from: undefined, }); const errObject = { @@ -566,8 +523,7 @@ describe('ElasticDatasource', () => { it('should not retry when ES is down', async () => { const twoDaysBefore = toUtc().subtract(2, 'day').format('YYYY.MM.DD'); - const { ds, timeSrv, fetchMock } = getTestContext({ - from: 'now-2w', + const { ds, fetchMock, timeRange } = getTestContext({ jsonData: { interval: 'Daily' }, fetchMockImplementation: (options) => { if (options.url === `${ELASTICSEARCH_MOCK_URL}/asd-${twoDaysBefore}/_mapping`) { @@ -577,9 +533,7 @@ describe('ElasticDatasource', () => { }, }); - const range = timeSrv.timeRange(); - - await expect(ds.getFields(undefined, range)).toEmitValuesWith((received) => { + await expect(ds.getFields(undefined, timeRange)).toEmitValuesWith((received) => { expect(received.length).toBe(1); expect(received[0]).toStrictEqual({ status: 500 }); expect(fetchMock).toBeCalledTimes(1); @@ -587,16 +541,16 @@ describe('ElasticDatasource', () => { }); it('should not retry more than 7 indices', async () => { - const { ds, timeSrv, fetchMock } = getTestContext({ - from: 'now-2w', + const { ds, fetchMock } = getTestContext({ jsonData: { interval: 'Daily' }, fetchMockImplementation: (options) => { return throwError({ status: 404 }); }, }); - const range = timeSrv.timeRange(); - await expect(ds.getFields(undefined, range)).toEmitValuesWith((received) => { + const timeRange = createTimeRange(dateTime().subtract(2, 'week'), dateTime()); + + await expect(ds.getFields(undefined, timeRange)).toEmitValuesWith((received) => { expect(received.length).toBe(1); expect(received[0]).toStrictEqual('Could not find an available index for this time range.'); expect(fetchMock).toBeCalledTimes(7); @@ -893,11 +847,8 @@ describe('ElasticDatasource', () => { }); it('should correctly add ad hoc filters when interpolating variables in query', () => { - const templateSrvMock = { - replace: (text?: string) => text, - getAdhocFilters: () => [{ key: 'bar', operator: '=', value: 'test' }], - } as unknown as TemplateSrv; - const { ds } = getTestContext({ templateSrvMock }); + const adHocFilters = [{ key: 'bar', operator: '=', value: 'test' }]; + const { ds } = getTestContext(); const query: ElasticsearchQuery = { refId: 'A', bucketAggs: [{ type: 'filters', settings: { filters: [{ query: '$var', label: '' }] }, id: '1' }], @@ -905,7 +856,7 @@ describe('ElasticDatasource', () => { query: 'foo:"bar"', }; - const interpolatedQuery = ds.interpolateVariablesInQueries([query], {})[0]; + const interpolatedQuery = ds.interpolateVariablesInQueries([query], {}, adHocFilters)[0]; expect(interpolatedQuery.query).toBe('foo:"bar" AND bar:"test"'); }); @@ -1307,73 +1258,59 @@ describe('queryHasFilter()', () => { describe('addAdhocFilters', () => { describe('with invalid filters', () => { - let ds: ElasticDatasource, templateSrv: TemplateSrv; + let ds: ElasticDatasource; beforeEach(() => { const context = getTestContext(); ds = context.ds; - templateSrv = context.templateSrv; }); it('should filter out ad hoc filter without key', () => { - jest.mocked(templateSrv.getAdhocFilters).mockReturnValue([{ key: '', operator: '=', value: 'a', condition: '' }]); - - const query = ds.addAdHocFilters('foo:"bar"'); + const query = ds.addAdHocFilters('foo:"bar"', [{ key: '', operator: '=', value: 'a', condition: '' }]); expect(query).toBe('foo:"bar"'); }); it('should filter out ad hoc filter without value', () => { - jest.mocked(templateSrv.getAdhocFilters).mockReturnValue([{ key: 'a', operator: '=', value: '', condition: '' }]); - - const query = ds.addAdHocFilters('foo:"bar"'); + const query = ds.addAdHocFilters('foo:"bar"', [{ key: 'a', operator: '=', value: '', condition: '' }]); expect(query).toBe('foo:"bar"'); }); it('should filter out filter ad hoc filter with invalid operator', () => { - jest.mocked(templateSrv.getAdhocFilters).mockReturnValue([{ key: 'a', operator: 'A', value: '', condition: '' }]); - - const query = ds.addAdHocFilters('foo:"bar"'); + const query = ds.addAdHocFilters('foo:"bar"', [{ key: 'a', operator: 'A', value: '', condition: '' }]); expect(query).toBe('foo:"bar"'); }); }); describe('with 1 ad hoc filter', () => { - let ds: ElasticDatasource, templateSrvMock: TemplateSrv; + let ds: ElasticDatasource; beforeEach(() => { - const { ds: datasource, templateSrv } = getTestContext(); + const { ds: datasource } = getTestContext(); ds = datasource; - templateSrvMock = templateSrv; - jest - .mocked(templateSrv.getAdhocFilters) - .mockReturnValue([{ key: 'test', operator: '=', value: 'test1', condition: '' }]); }); it('should correctly add 1 ad hoc filter when query is not empty', () => { - const query = ds.addAdHocFilters('foo:"bar"'); + const filters = [{ key: 'test', operator: '=', value: 'test1', condition: '' }]; + const query = ds.addAdHocFilters('foo:"bar"', filters); expect(query).toBe('foo:"bar" AND test:"test1"'); }); it('should correctly add 1 ad hoc filter when query is empty', () => { - expect(ds.addAdHocFilters('')).toBe('test:"test1"'); - expect(ds.addAdHocFilters(' ')).toBe('test:"test1"'); - expect(ds.addAdHocFilters(' ')).toBe('test:"test1"'); + const filters = [{ key: 'test', operator: '=', value: 'test1', condition: '' }]; + expect(ds.addAdHocFilters('', filters)).toBe('test:"test1"'); + expect(ds.addAdHocFilters(' ', filters)).toBe('test:"test1"'); + expect(ds.addAdHocFilters(' ', filters)).toBe('test:"test1"'); }); it('should not fail if the filter value is a number', () => { - jest - .mocked(templateSrvMock.getAdhocFilters) - // @ts-expect-error - .mockReturnValue([{ key: 'key', operator: '=', value: 1, condition: '' }]); - expect(ds.addAdHocFilters('')).toBe('key:"1"'); + // @ts-expect-error + expect(ds.addAdHocFilters('', [{ key: 'key', operator: '=', value: 1, condition: '' }])).toBe('key:"1"'); }); it.each(['=', '!=', '=~', '!~', '>', '<', '', ''])( `should properly build queries with '%s' filters`, (operator: string) => { - jest - .mocked(templateSrvMock.getAdhocFilters) - .mockReturnValue([{ key: 'key', operator, value: 'value', condition: '' }]); + const filters = [{ key: 'key', operator, value: 'value', condition: '' }]; + const query = ds.addAdHocFilters('foo:"bar"', filters); - const query = ds.addAdHocFilters('foo:"bar"'); switch (operator) { case '=': expect(query).toBe('foo:"bar" AND key:"value"'); @@ -1398,46 +1335,40 @@ describe('addAdhocFilters', () => { ); it('should escape characters in filter keys', () => { - jest - .mocked(templateSrvMock.getAdhocFilters) - .mockReturnValue([{ key: 'field:name', operator: '=', value: 'field:value', condition: '' }]); - - const query = ds.addAdHocFilters(''); + const filters = [{ key: 'field:name', operator: '=', value: 'field:value', condition: '' }]; + const query = ds.addAdHocFilters('', filters); expect(query).toBe('field\\:name:"field:value"'); }); it('should escape characters in filter values', () => { - jest - .mocked(templateSrvMock.getAdhocFilters) - .mockReturnValue([{ key: 'field:name', operator: '=', value: 'field "value"', condition: '' }]); - - const query = ds.addAdHocFilters(''); + const filters = [{ key: 'field:name', operator: '=', value: 'field "value"', condition: '' }]; + const query = ds.addAdHocFilters('', filters); expect(query).toBe('field\\:name:"field \\"value\\""'); }); }); describe('with multiple ad hoc filters', () => { let ds: ElasticDatasource; + const filters = [ + { key: 'bar', operator: '=', value: 'baz', condition: '' }, + { key: 'job', operator: '!=', value: 'grafana', condition: '' }, + { key: 'service', operator: '=~', value: 'service', condition: '' }, + { key: 'count', operator: '>', value: '1', condition: '' }, + ]; beforeEach(() => { - const { ds: datasource, templateSrv } = getTestContext(); + const { ds: datasource } = getTestContext(); ds = datasource; - jest.mocked(templateSrv.getAdhocFilters).mockReturnValue([ - { key: 'bar', operator: '=', value: 'baz', condition: '' }, - { key: 'job', operator: '!=', value: 'grafana', condition: '' }, - { key: 'service', operator: '=~', value: 'service', condition: '' }, - { key: 'count', operator: '>', value: '1', condition: '' }, - ]); }); it('should correctly add ad hoc filters when query is not empty', () => { - const query = ds.addAdHocFilters('foo:"bar" AND test:"test1"'); + const query = ds.addAdHocFilters('foo:"bar" AND test:"test1"', filters); expect(query).toBe( 'foo:"bar" AND test:"test1" AND bar:"baz" AND -job:"grafana" AND service:/service/ AND count:>1' ); }); it('should correctly add ad hoc filters when query is empty', () => { - const query = ds.addAdHocFilters(''); + const query = ds.addAdHocFilters('', filters); expect(query).toBe('bar:"baz" AND -job:"grafana" AND service:/service/ AND count:>1'); }); }); @@ -1591,7 +1522,7 @@ describe('ElasticDatasource using backend', () => { describe('annotationQuery', () => { describe('results processing', () => { it('should return simple annotations using defaults', async () => { - const { ds, timeSrv } = getTestContext(); + const { ds, timeRange } = getTestContext(); ds.postResourceRequest = jest.fn().mockResolvedValue({ responses: [ { @@ -1607,7 +1538,7 @@ describe('ElasticDatasource using backend', () => { const annotations = await ds.annotationQuery({ annotation: {}, - range: timeSrv.timeRange(), + range: timeRange, }); expect(annotations).toHaveLength(2); @@ -1616,7 +1547,7 @@ describe('ElasticDatasource using backend', () => { }); it('should return annotation events using options', async () => { - const { ds, timeSrv } = getTestContext(); + const { ds, timeRange } = getTestContext(); ds.postResourceRequest = jest.fn().mockResolvedValue({ responses: [ { @@ -1638,7 +1569,7 @@ describe('ElasticDatasource using backend', () => { tagsField: '@test_tags', textField: 'text', }, - range: timeSrv.timeRange(), + range: timeRange, }); expect(annotations).toHaveLength(2); expect(annotations[0].time).toBe(1); @@ -1877,8 +1808,7 @@ describe('ElasticDatasource using backend', () => { it('should not retry when ES is down', async () => { const twoDaysBefore = toUtc().subtract(2, 'day').format('YYYY.MM.DD'); - const { ds, timeSrv } = getTestContext({ - from: 'now-2w', + const { ds, timeRange } = getTestContext({ jsonData: { interval: 'Daily' }, }); @@ -1891,8 +1821,7 @@ describe('ElasticDatasource using backend', () => { return throwError({ status: 500 }); }); - const range = timeSrv.timeRange(); - await expect(ds.getFields(undefined, range)).toEmitValuesWith((received) => { + await expect(ds.getFields(undefined, timeRange)).toEmitValuesWith((received) => { expect(received.length).toBe(1); expect(received[0]).toStrictEqual({ status: 500 }); expect(ds.getResource).toBeCalledTimes(1); @@ -1900,17 +1829,17 @@ describe('ElasticDatasource using backend', () => { }); it('should not retry more than 7 indices', async () => { - const { ds, timeSrv } = getTestContext({ - from: 'now-2w', + const { ds } = getTestContext({ jsonData: { interval: 'Daily' }, }); - const range = timeSrv.timeRange(); ds.getResource = jest.fn().mockImplementation(() => { return throwError({ status: 404 }); }); - await expect(ds.getFields(undefined, range)).toEmitValuesWith((received) => { + const timeRange = createTimeRange(dateTime().subtract(2, 'week'), dateTime()); + + await expect(ds.getFields(undefined, timeRange)).toEmitValuesWith((received) => { expect(received.length).toBe(1); expect(received[0]).toStrictEqual('Could not find an available index for this time range.'); expect(ds.getResource).toBeCalledTimes(7); @@ -1919,7 +1848,6 @@ describe('ElasticDatasource using backend', () => { it('should return nested fields', async () => { const { ds } = getTestContext({ - from: 'now-2w', jsonData: { interval: 'Daily' }, }); diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index 513c5393446..deb2319f8fe 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -35,10 +35,17 @@ import { DataSourceWithToggleableQueryFiltersSupport, QueryFilterOptions, ToggleFilterAction, + DataSourceGetTagValuesOptions, + AdHocVariableFilter, } from '@grafana/data'; -import { DataSourceWithBackend, getDataSourceSrv, config, BackendSrvRequest } from '@grafana/runtime'; -import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; -import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; +import { + DataSourceWithBackend, + getDataSourceSrv, + config, + BackendSrvRequest, + TemplateSrv, + getTemplateSrv, +} from '@grafana/runtime'; import { queryLogsSample, queryLogsVolume } from '../../../features/logs/logsModel'; import { getLogLevelFromKey } from '../../../features/logs/utils'; @@ -114,7 +121,6 @@ export class ElasticDatasource languageProvider: LanguageProvider; includeFrozen: boolean; isProxyAccess: boolean; - timeSrv: TimeSrv; databaseVersion: SemVer | null; legacyQueryRunner: LegacyQueryRunner; @@ -157,7 +163,6 @@ export class ElasticDatasource this.logLevelField = undefined; } this.languageProvider = new LanguageProvider(this); - this.timeSrv = getTimeSrv(); this.legacyQueryRunner = new LegacyQueryRunner(this, this.templateSrv); } @@ -399,8 +404,12 @@ export class ElasticDatasource return this.templateSrv.replace(queryString, scopedVars, 'lucene'); } - interpolateVariablesInQueries(queries: ElasticsearchQuery[], scopedVars: ScopedVars | {}): ElasticsearchQuery[] { - return queries.map((q) => this.applyTemplateVariables(q, scopedVars)); + interpolateVariablesInQueries( + queries: ElasticsearchQuery[], + scopedVars: ScopedVars, + filters?: AdHocVariableFilter[] + ): ElasticsearchQuery[] { + return queries.map((q) => this.applyTemplateVariables(q, scopedVars, filters)); } async testDatasource() { @@ -832,9 +841,8 @@ export class ElasticDatasource return lastValueFrom(this.getFields()); } - getTagValues(options: { key: string }) { - const range = this.timeSrv.timeRange(); - return lastValueFrom(this.getTerms({ field: options.key }, range)); + getTagValues(options: DataSourceGetTagValuesOptions) { + return lastValueFrom(this.getTerms({ field: options.key }, options.timeRange)); } targetContainsTemplate(target: ElasticsearchQuery) { @@ -944,9 +952,8 @@ export class ElasticDatasource return { ...query, query: expression }; } - addAdHocFilters(query: string) { - const adhocFilters = this.templateSrv.getAdhocFilters(this.name); - if (adhocFilters.length === 0) { + addAdHocFilters(query: string, adhocFilters?: AdHocVariableFilter[]) { + if (!adhocFilters) { return query; } let finalQuery = query; @@ -958,7 +965,11 @@ export class ElasticDatasource } // Used when running queries through backend - applyTemplateVariables(query: ElasticsearchQuery, scopedVars: ScopedVars): ElasticsearchQuery { + applyTemplateVariables( + query: ElasticsearchQuery, + scopedVars: ScopedVars, + filters?: AdHocVariableFilter[] + ): ElasticsearchQuery { // We need a separate interpolation format for lucene queries, therefore we first interpolate any // lucene query string and then everything else const interpolateBucketAgg = (bucketAgg: BucketAggregation): BucketAggregation => { @@ -981,7 +992,7 @@ export class ElasticDatasource const expandedQuery = { ...query, datasource: this.getRef(), - query: this.addAdHocFilters(this.interpolateLuceneQuery(query.query || '', scopedVars)), + query: this.addAdHocFilters(this.interpolateLuceneQuery(query.query || '', scopedVars), filters), bucketAggs: query.bucketAggs?.map(interpolateBucketAgg), }; diff --git a/public/app/plugins/datasource/elasticsearch/mocks.ts b/public/app/plugins/datasource/elasticsearch/mocks.ts index 29f7242181f..a33e258c449 100644 --- a/public/app/plugins/datasource/elasticsearch/mocks.ts +++ b/public/app/plugins/datasource/elasticsearch/mocks.ts @@ -1,13 +1,10 @@ import { DataSourceInstanceSettings, PluginType } from '@grafana/data'; -import { TemplateSrv } from 'app/features/templating/template_srv'; +import { TemplateSrv } from '@grafana/runtime'; import { ElasticDatasource } from './datasource'; import { ElasticsearchOptions } from './types'; -export function createElasticDatasource( - settings: Partial> = {}, - templateSrv: TemplateSrv -) { +export function createElasticDatasource(settings: Partial> = {}) { const { jsonData, ...rest } = settings; const instanceSettings: DataSourceInstanceSettings = { @@ -48,5 +45,18 @@ export function createElasticDatasource( ...rest, }; + const templateSrv: TemplateSrv = { + getVariables: () => [], + replace: (text?: string) => { + if (text?.startsWith('$')) { + return `resolvedVariable`; + } else { + return text || ''; + } + }, + containsTemplate: (text?: string) => text?.includes('$') ?? false, + updateTimeRange: () => {}, + }; + return new ElasticDatasource(instanceSettings, templateSrv); } From fbbc524cb618914c7d10bde3aa38154dd8b20b66 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 24 Oct 2023 09:27:23 +0100 Subject: [PATCH 005/180] Navigation: Default `MegaMenu` to `docked` when screensize > 1440 (#76960) default megamenu to docked when screensize > 1440 --- public/app/core/components/AppChrome/AppChromeService.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx index c39a25766d0..9e19c5d0f16 100644 --- a/public/app/core/components/AppChrome/AppChromeService.tsx +++ b/public/app/core/components/AppChrome/AppChromeService.tsx @@ -34,7 +34,10 @@ export class AppChromeService { sectionNav: { node: { text: t('nav.home.title', 'Home') }, main: { text: '' } }, searchBarHidden: store.getBool(this.searchBarStorageKey, false), megaMenu: - config.featureToggles.dockedMegaMenu && store.getBool(DOCKED_LOCAL_STORAGE_KEY, false) ? 'docked' : 'closed', + config.featureToggles.dockedMegaMenu && + store.getBool(DOCKED_LOCAL_STORAGE_KEY, window.innerWidth > config.theme2.breakpoints.values.xxl) + ? 'docked' + : 'closed', kioskMode: null, layout: PageLayoutType.Canvas, }); From bd2b4e956bd2fdfd063017856809f4d90118f31e Mon Sep 17 00:00:00 2001 From: Horst Gutmann Date: Tue, 24 Oct 2023 10:52:14 +0200 Subject: [PATCH 006/180] =?UTF-8?q?CI:=20rgm-package=20must=20wait=20for?= =?UTF-8?q?=20update-package-json-version=20in=20main=20pip=E2=80=A6=20(#7?= =?UTF-8?q?7022)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI: rgm-package must wait for update-package-json-version in main pipeline --- .drone.yml | 4 ++-- scripts/drone/pipelines/build.star | 3 ++- scripts/drone/steps/rgm.star | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.drone.yml b/.drone.yml index ed10ff276f3..ead82b92296 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1748,7 +1748,7 @@ steps: --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER --grafana-dir=$$PWD > packages.txt depends_on: - - yarn-install + - update-package-json-version image: grafana/grafana-build:main name: rgm-package pull: always @@ -4607,6 +4607,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: a23a1640b21a7e8dd62089ab21b84a4efc43504cd6e956dd0f49e56df4f4e5a2 +hmac: 93de8a710e23d3f1d31860f9eed34cd841b0a0eb48637971de4e8ce60a7c3df1 ... diff --git a/scripts/drone/pipelines/build.star b/scripts/drone/pipelines/build.star index 88b5ed8e52a..ee60b7b5103 100644 --- a/scripts/drone/pipelines/build.star +++ b/scripts/drone/pipelines/build.star @@ -70,17 +70,18 @@ def build_e2e(trigger, ver_mode): [ build_frontend_package_step(), enterprise_downstream_step(ver_mode = ver_mode), + rgm_package_step(distros = "linux/amd64,linux/arm64", file = "packages.txt"), ], ) else: build_steps.extend([ update_package_json_version(), build_frontend_package_step(depends_on = ["update-package-json-version"]), + rgm_package_step(depends_on = ["update-package-json-version"], distros = "linux/amd64,linux/arm64", file = "packages.txt"), ]) build_steps.extend( [ - rgm_package_step(distros = "linux/amd64,linux/arm64", file = "packages.txt"), grafana_server_step(), e2e_tests_step("dashboards-suite"), e2e_tests_step("smoke-tests-suite"), diff --git a/scripts/drone/steps/rgm.star b/scripts/drone/steps/rgm.star index 495b443e4a0..6d86ac48b34 100644 --- a/scripts/drone/steps/rgm.star +++ b/scripts/drone/steps/rgm.star @@ -9,12 +9,12 @@ load( ) # rgm_package_step will create a tar.gz for use in e2e tests or other PR testing related activities.. -def rgm_package_step(distros = "linux/amd64,linux/arm64", file = "packages.txt"): +def rgm_package_step(distros = "linux/amd64,linux/arm64", file = "packages.txt", depends_on = ["yarn-install"]): return { "name": "rgm-package", "image": "grafana/grafana-build:main", "pull": "always", - "depends_on": ["yarn-install"], + "depends_on": depends_on, "commands": [ "/src/grafana-build package --distro={} ".format(distros) + "--go-version={} ".format(golang_version) + From 159bb3c032fe0f66134209b12411619767090b3e Mon Sep 17 00:00:00 2001 From: Ieva Date: Tue, 24 Oct 2023 09:55:38 +0100 Subject: [PATCH 007/180] RBAC: Allow scoping access to root level dashboards (#76987) * correctly check permissions to list dashboards on the root * correctly display the access inherited from general folder for dashboards * Update pkg/services/sqlstore/permissions/dashboard.go Co-authored-by: Gabriel MABILLE * Update dashboard_filter_no_subquery.go --------- Co-authored-by: Gabriel MABILLE --- .../ossaccesscontrol/permissions_services.go | 2 +- .../sqlstore/permissions/dashboard.go | 18 +++++ .../dashboard_filter_no_subquery.go | 5 ++ .../sqlstore/permissions/dashboard_test.go | 70 ++++++++++++++++--- 4 files changed, 84 insertions(+), 11 deletions(-) diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index 35c4b9cd657..feb864e6d91 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -163,7 +163,7 @@ func ProvideDashboardPermissions( } return append([]string{parentScope}, nestedScopes...), nil } - return []string{}, nil + return []string{dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, nil }, Assignments: resourcepermissions.Assignments{ Users: true, diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go index 9c1fa1d93fc..cf12f5bcd05 100644 --- a/pkg/services/sqlstore/permissions/dashboard.go +++ b/pkg/services/sqlstore/permissions/dashboard.go @@ -5,6 +5,8 @@ import ( "fmt" "strings" + "golang.org/x/exp/slices" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/dashboards" @@ -229,6 +231,11 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { } } builder.WriteString(") AND NOT dashboard.is_folder)") + + // Include all the dashboards under the root if the user has the required permissions on the root (used to be the General folder) + if hasAccessToRoot(toCheck, f.user) { + builder.WriteString(" OR (dashboard.folder_id = 0 AND NOT dashboard.is_folder)") + } } else { builder.WriteString("NOT dashboard.is_folder") } @@ -423,3 +430,14 @@ func getAllowedUIDs(actions []string, user identity.Requester, scopePrefix strin } return args } + +// Checks if the user has the required permissions on the root (used to be the General folder) +func hasAccessToRoot(actionsToCheck []any, user identity.Requester) bool { + generalFolderScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID) + for _, action := range actionsToCheck { + if !slices.Contains(user.GetPermissions()[action.(string)], generalFolderScope) { + return false + } + } + return true +} diff --git a/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go b/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go index 7b885755ae1..2507c21fba9 100644 --- a/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go +++ b/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go @@ -146,6 +146,11 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses() } builder.WriteString(" AND NOT dashboard.is_folder)") } + + // Include all the dashboards under the root if the user has the required permissions on the root (used to be the General folder) + if hasAccessToRoot(toCheck, f.user) { + builder.WriteString(" OR (dashboard.folder_id = 0 AND NOT dashboard.is_folder)") + } } else { builder.WriteString("NOT dashboard.is_folder") } diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index e960be75715..a47c3182190 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -52,7 +52,7 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { permissions: []accesscontrol.Permission{ {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeDashboardsAll}, }, - expectedResult: 100, + expectedResult: 110, }, { desc: "Should be able to view all dashboards with folder wildcard scope", @@ -60,7 +60,32 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { permissions: []accesscontrol.Permission{ {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll}, }, - expectedResult: 100, + expectedResult: 110, + }, + { + desc: "Should be able to view dashboards under the root with folders:uid:general scope", + permission: dashboards.PERMISSION_VIEW, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, + }, + expectedResult: 10, + }, + { + desc: "Should not be able to view editable dashboards under the root with folders:uid:general scope if missing write action", + permission: dashboards.PERMISSION_EDIT, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, + }, + expectedResult: 0, + }, + { + desc: "Should be able to view editable dashboards under the root with folders:uid:general scope if has write action", + permission: dashboards.PERMISSION_EDIT, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, + {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, + }, + expectedResult: 10, }, { desc: "Should be able to view a subset of dashboards with dashboard scopes", @@ -163,7 +188,7 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { } for _, tt := range tests { - store := setupTest(t, 10, 100, tt.permissions) + store := setupTest(t, 10, 110, tt.permissions) recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported() require.NoError(t, err) @@ -219,7 +244,7 @@ func TestIntegration_DashboardPermissionFilter_WithSelfContainedPermissions(t *t signedInUserPermissions: []accesscontrol.Permission{ {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeDashboardsAll}, }, - expectedResult: 100, + expectedResult: 110, }, { desc: "Should be able to view all dashboards with folder wildcard scope", @@ -227,7 +252,7 @@ func TestIntegration_DashboardPermissionFilter_WithSelfContainedPermissions(t *t signedInUserPermissions: []accesscontrol.Permission{ {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll}, }, - expectedResult: 100, + expectedResult: 110, }, { desc: "Should not be able to view any dashboards or folders without any permissions", @@ -258,6 +283,31 @@ func TestIntegration_DashboardPermissionFilter_WithSelfContainedPermissions(t *t }, expectedResult: 20, }, + { + desc: "Should be able to view dashboards under the root with folders:uid:general scope", + permission: dashboards.PERMISSION_VIEW, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, + }, + expectedResult: 10, + }, + { + desc: "Should not be able to view editable dashboards under the root with folders:uid:general scope if missing write action", + permission: dashboards.PERMISSION_EDIT, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, + }, + expectedResult: 0, + }, + { + desc: "Should be able to view editable dashboards under the root with folders:uid:general scope if has write action", + permission: dashboards.PERMISSION_EDIT, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, + {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, + }, + expectedResult: 10, + }, { desc: "Should be able to view all folders with folder wildcard", permission: dashboards.PERMISSION_VIEW, @@ -337,7 +387,7 @@ func TestIntegration_DashboardPermissionFilter_WithSelfContainedPermissions(t *t } for _, tt := range tests { - store := setupTest(t, 10, 100, []accesscontrol.Permission{}) + store := setupTest(t, 10, 110, []accesscontrol.Permission{}) recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported() require.NoError(t, err) @@ -717,12 +767,12 @@ func setupTest(t *testing.T, numFolders, numDashboards int, permissions []access Updated: time.Now(), }) } - // Seed 100 dashboard + // Seed dashboards for i := numFolders + 1; i <= numFolders+numDashboards; i++ { str := strconv.Itoa(i) - folderID := numFolders - if i%numFolders != 0 { - folderID = i % numFolders + folderID := 0 + if i%(numFolders+1) != 0 { + folderID = i % (numFolders + 1) } dashes = append(dashes, dashboards.Dashboard{ OrgID: 1, From 3015e5921f022b591571cce3cb639b4294c65097 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Tue, 24 Oct 2023 11:01:04 +0200 Subject: [PATCH 008/180] Chore: Move `extsvcaccounts` package to `serviceaccounts` (#76977) * Chore: Move extsvcaccounts package to serviceaccounts * Fix proxy * Fix tests * Fix linting --- pkg/server/wire.go | 4 +-- pkg/services/extsvcauth/errors.go | 3 +- pkg/services/extsvcauth/models.go | 26 ---------------- .../extsvcauth/oauthserver/oasimpl/service.go | 7 +++-- .../oauthserver/oasimpl/service_test.go | 16 +++++----- .../oauthserver/oasimpl/token_test.go | 4 +-- pkg/services/extsvcauth/registry/service.go | 2 +- .../extsvcaccounts/models.go | 5 ++-- .../extsvcaccounts/service.go | 30 +++++++++---------- .../extsvcaccounts/service_test.go | 30 +++++++++---------- pkg/services/serviceaccounts/models.go | 19 ++++++++++++ pkg/services/serviceaccounts/proxy/service.go | 6 ++-- .../serviceaccounts/proxy/service_test.go | 2 +- .../serviceaccounts/serviceaccounts.go | 8 +++++ .../tests}/extsvcaccmock.go | 22 +++++++------- 15 files changed, 93 insertions(+), 91 deletions(-) rename pkg/services/{extsvcauth => serviceaccounts}/extsvcaccounts/models.go (92%) rename pkg/services/{extsvcauth => serviceaccounts}/extsvcaccounts/service.go (91%) rename pkg/services/{extsvcauth => serviceaccounts}/extsvcaccounts/service_test.go (93%) rename pkg/services/{extsvcauth/extsvcmocks => serviceaccounts/tests}/extsvcaccmock.go (67%) diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 36bc7e0e4f7..24231f93fbe 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -64,7 +64,6 @@ import ( "github.com/grafana/grafana/pkg/services/encryption" encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service" "github.com/grafana/grafana/pkg/services/extsvcauth" - "github.com/grafana/grafana/pkg/services/extsvcauth/extsvcaccounts" "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/oasimpl" extsvcreg "github.com/grafana/grafana/pkg/services/extsvcauth/registry" @@ -122,6 +121,7 @@ import ( secretsMigrations "github.com/grafana/grafana/pkg/services/secrets/kvstore/migrations" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/serviceaccounts" + "github.com/grafana/grafana/pkg/services/serviceaccounts/extsvcaccounts" serviceaccountsmanager "github.com/grafana/grafana/pkg/services/serviceaccounts/manager" serviceaccountsretriever "github.com/grafana/grafana/pkg/services/serviceaccounts/retriever" "github.com/grafana/grafana/pkg/services/shorturls" @@ -367,7 +367,7 @@ var wireBasicSet = wire.NewSet( authnimpl.ProvideAuthnService, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, - wire.Bind(new(extsvcauth.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), + wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), oasimpl.ProvideService, wire.Bind(new(oauthserver.OAuth2Server), new(*oasimpl.OAuth2ServiceImpl)), extsvcreg.ProvideExtSvcRegistry, diff --git a/pkg/services/extsvcauth/errors.go b/pkg/services/extsvcauth/errors.go index c605dd3f7ab..f1af61f4f0c 100644 --- a/pkg/services/extsvcauth/errors.go +++ b/pkg/services/extsvcauth/errors.go @@ -3,6 +3,5 @@ package extsvcauth import "github.com/grafana/grafana/pkg/util/errutil" var ( - ErrUnknownProvider = errutil.BadRequest("extsvcauth.unknown-provider") - ErrCredentialsNotFound = errutil.NotFound("extsvcauth.credentials-not-found") + ErrUnknownProvider = errutil.BadRequest("extsvcauth.unknown-provider") ) diff --git a/pkg/services/extsvcauth/models.go b/pkg/services/extsvcauth/models.go index e316aa6b7b8..fd69ec4efe5 100644 --- a/pkg/services/extsvcauth/models.go +++ b/pkg/services/extsvcauth/models.go @@ -3,7 +3,6 @@ package extsvcauth import ( "context" - "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/accesscontrol" ) @@ -24,31 +23,6 @@ type ExternalServiceRegistry interface { SaveExternalService(ctx context.Context, cmd *ExternalServiceRegistration) (*ExternalService, error) } -//go:generate mockery --name ExtSvcAccountsService --structname MockExtSvcAccountsService --output extsvcmocks --outpkg extsvcmocks --filename extsvcaccmock.go -type ExtSvcAccountsService interface { - // ManageExtSvcAccount creates, updates or deletes the service account associated with an external service - ManageExtSvcAccount(ctx context.Context, cmd *ManageExtSvcAccountCmd) (int64, error) - // RetrieveExtSvcAccount fetches an external service account by ID - RetrieveExtSvcAccount(ctx context.Context, orgID, saID int64) (*ExtSvcAccount, error) -} - -// ExtSvcAccount represents the service account associated to an external service -type ExtSvcAccount struct { - ID int64 - Login string - Name string - OrgID int64 - IsDisabled bool - Role roletype.RoleType -} - -type ManageExtSvcAccountCmd struct { - ExtSvcSlug string - Enabled bool // disabled: the service account and its permissions will be deleted - OrgID int64 - Permissions []accesscontrol.Permission -} - type SelfCfg struct { // Enabled allows the service to request access tokens for itself Enabled bool diff --git a/pkg/services/extsvcauth/oauthserver/oasimpl/service.go b/pkg/services/extsvcauth/oauthserver/oasimpl/service.go index a0d4312057c..9aae5e8feae 100644 --- a/pkg/services/extsvcauth/oauthserver/oasimpl/service.go +++ b/pkg/services/extsvcauth/oauthserver/oasimpl/service.go @@ -33,6 +33,7 @@ import ( "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/store" "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/utils" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/signingkeys" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/user" @@ -54,14 +55,14 @@ type OAuth2ServiceImpl struct { logger log.Logger accessControl ac.AccessControl acService ac.Service - saService extsvcauth.ExtSvcAccountsService + saService serviceaccounts.ExtSvcAccountsService userService user.Service teamService team.Service publicKey any } func ProvideService(router routing.RouteRegister, db db.DB, cfg *setting.Cfg, - extSvcAccSvc extsvcauth.ExtSvcAccountsService, accessControl ac.AccessControl, acSvc ac.Service, userSvc user.Service, + extSvcAccSvc serviceaccounts.ExtSvcAccountsService, accessControl ac.AccessControl, acSvc ac.Service, userSvc user.Service, teamSvc team.Service, keySvc signingkeys.Service, fmgmt *featuremgmt.FeatureManager) (*OAuth2ServiceImpl, error) { if !fmgmt.IsEnabled(featuremgmt.FlagExternalServiceAuth) { return nil, nil @@ -245,7 +246,7 @@ func (s *OAuth2ServiceImpl) SaveExternalService(ctx context.Context, registratio client.Secret = string(hashedSecret) s.logger.Debug("Save service account") - saID, errSaveServiceAccount := s.saService.ManageExtSvcAccount(ctx, &extsvcauth.ManageExtSvcAccountCmd{ + saID, errSaveServiceAccount := s.saService.ManageExtSvcAccount(ctx, &serviceaccounts.ManageExtSvcAccountCmd{ ExtSvcSlug: slugify.Slugify(client.Name), Enabled: registration.Self.Enabled, OrgID: oauthserver.TmpOrgID, diff --git a/pkg/services/extsvcauth/oauthserver/oasimpl/service_test.go b/pkg/services/extsvcauth/oauthserver/oasimpl/service_test.go index c97c5cc2c78..afcedd06cc2 100644 --- a/pkg/services/extsvcauth/oauthserver/oasimpl/service_test.go +++ b/pkg/services/extsvcauth/oauthserver/oasimpl/service_test.go @@ -21,11 +21,11 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/extsvcauth" - "github.com/grafana/grafana/pkg/services/extsvcauth/extsvcmocks" "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/oastest" "github.com/grafana/grafana/pkg/services/featuremgmt" sa "github.com/grafana/grafana/pkg/services/serviceaccounts" + saTests "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" "github.com/grafana/grafana/pkg/services/signingkeys/signingkeystest" "github.com/grafana/grafana/pkg/services/team/teamtest" "github.com/grafana/grafana/pkg/services/user" @@ -50,7 +50,7 @@ type TestEnv struct { OAuthStore *oastest.MockStore UserService *usertest.FakeUserService TeamService *teamtest.FakeService - SAService *extsvcmocks.MockExtSvcAccountsService + SAService *saTests.MockExtSvcAccountsService } func setupTestEnv(t *testing.T) *TestEnv { @@ -75,7 +75,7 @@ func setupTestEnv(t *testing.T) *TestEnv { OAuthStore: &oastest.MockStore{}, UserService: usertest.NewUserServiceFake(), TeamService: teamtest.NewFakeService(), - SAService: extsvcmocks.NewMockExtSvcAccountsService(t), + SAService: saTests.NewMockExtSvcAccountsService(t), } env.S = &OAuth2ServiceImpl{ cache: localcache.New(cacheExpirationTime, cacheCleanupInterval), @@ -166,7 +166,7 @@ func TestOAuth2ServiceImpl_SaveExternalService(t *testing.T) { })) // Check that despite no credential_grants the service account still has a permission to impersonate users env.SAService.AssertCalled(t, "ManageExtSvcAccount", mock.Anything, - mock.MatchedBy(func(cmd *extsvcauth.ManageExtSvcAccountCmd) bool { + mock.MatchedBy(func(cmd *sa.ManageExtSvcAccountCmd) bool { return len(cmd.Permissions) == 1 && cmd.Permissions[0] == ac.Permission{Action: ac.ActionUsersRead, Scope: ac.ScopeUsersAll} })) }, @@ -200,7 +200,7 @@ func TestOAuth2ServiceImpl_SaveExternalService(t *testing.T) { })) // Check that despite no credential_grants the service account still has a permission to impersonate users env.SAService.AssertCalled(t, "ManageExtSvcAccount", mock.Anything, - mock.MatchedBy(func(cmd *extsvcauth.ManageExtSvcAccountCmd) bool { + mock.MatchedBy(func(cmd *sa.ManageExtSvcAccountCmd) bool { return len(cmd.Permissions) == 1 && cmd.Permissions[0] == ac.Permission{Action: ac.ActionUsersImpersonate, Scope: ac.ScopeUsersAll} })) }, @@ -318,7 +318,7 @@ func TestOAuth2ServiceImpl_GetExternalService(t *testing.T) { name: "should return error when the service account was not found", init: func(env *TestEnv) { env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(dummyClient(), nil) - env.SAService.On("RetrieveExtSvcAccount", mock.Anything, int64(1), int64(1)).Return(&extsvcauth.ExtSvcAccount{}, sa.ErrServiceAccountNotFound) + env.SAService.On("RetrieveExtSvcAccount", mock.Anything, int64(1), int64(1)).Return(&sa.ExtSvcAccount{}, sa.ErrServiceAccountNotFound) }, mockChecks: func(t *testing.T, env *TestEnv) { env.OAuthStore.AssertCalled(t, "GetExternalService", mock.Anything, mock.Anything) @@ -330,7 +330,7 @@ func TestOAuth2ServiceImpl_GetExternalService(t *testing.T) { name: "should return error when the service account has no permissions", init: func(env *TestEnv) { env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(dummyClient(), nil) - env.SAService.On("RetrieveExtSvcAccount", mock.Anything, int64(1), int64(1)).Return(&extsvcauth.ExtSvcAccount{}, nil) + env.SAService.On("RetrieveExtSvcAccount", mock.Anything, int64(1), int64(1)).Return(&sa.ExtSvcAccount{}, nil) env.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything).Return(nil, fmt.Errorf("some error")) }, mockChecks: func(t *testing.T, env *TestEnv) { @@ -343,7 +343,7 @@ func TestOAuth2ServiceImpl_GetExternalService(t *testing.T) { name: "should return correctly", init: func(env *TestEnv) { env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(dummyClient(), nil) - env.SAService.On("RetrieveExtSvcAccount", mock.Anything, int64(1), int64(1)).Return(&extsvcauth.ExtSvcAccount{ID: 1}, nil) + env.SAService.On("RetrieveExtSvcAccount", mock.Anything, int64(1), int64(1)).Return(&sa.ExtSvcAccount{ID: 1}, nil) env.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything).Return([]ac.Permission{{Action: ac.ActionUsersImpersonate, Scope: ac.ScopeUsersAll}}, nil) }, mockChecks: func(t *testing.T, env *TestEnv) { diff --git a/pkg/services/extsvcauth/oauthserver/oasimpl/token_test.go b/pkg/services/extsvcauth/oauthserver/oasimpl/token_test.go index 5d0b68d4944..ac49dfddb5d 100644 --- a/pkg/services/extsvcauth/oauthserver/oasimpl/token_test.go +++ b/pkg/services/extsvcauth/oauthserver/oasimpl/token_test.go @@ -22,8 +22,8 @@ import ( "github.com/grafana/grafana/pkg/models/roletype" ac "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/extsvcauth" "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" + sa "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/user" ) @@ -703,7 +703,7 @@ func setupHandleTokenRequestEnv(t *testing.T, env *TestEnv, opt func(*oauthserve opt(client1) } - sa1 := &extsvcauth.ExtSvcAccount{ + sa1 := &sa.ExtSvcAccount{ ID: client1.ServiceAccountID, Name: client1.Name, Login: client1.Name, diff --git a/pkg/services/extsvcauth/registry/service.go b/pkg/services/extsvcauth/registry/service.go index 0cb3b9db911..efeb7c1771d 100644 --- a/pkg/services/extsvcauth/registry/service.go +++ b/pkg/services/extsvcauth/registry/service.go @@ -5,9 +5,9 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/extsvcauth" - "github.com/grafana/grafana/pkg/services/extsvcauth/extsvcaccounts" "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/serviceaccounts/extsvcaccounts" ) var _ extsvcauth.ExternalServiceRegistry = &Registry{} diff --git a/pkg/services/extsvcauth/extsvcaccounts/models.go b/pkg/services/serviceaccounts/extsvcaccounts/models.go similarity index 92% rename from pkg/services/extsvcauth/extsvcaccounts/models.go rename to pkg/services/serviceaccounts/extsvcaccounts/models.go index 9211ae6cc2a..8ea0cb77d36 100644 --- a/pkg/services/extsvcauth/extsvcaccounts/models.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/models.go @@ -7,8 +7,7 @@ import ( ) const ( - ExtSvcPrefix = "extsvc-" - kvStoreType = "extsvc-token" + kvStoreType = "extsvc-token" // #nosec G101 - this is not a hardcoded secret tokenNamePrefix = "extsvc-token" ) @@ -18,6 +17,8 @@ var ( ErrInvalidName = errutil.BadRequest("extsvcaccounts.ErrInvalidName", errutil.WithPublicMessage("only external service account names can be prefixed with 'extsvc-'")) ErrCannotBeUpdated = errutil.BadRequest("extsvcaccounts.ErrCannotBeUpdated", errutil.WithPublicMessage("external service account cannot be updated")) ErrCannotCreateToken = errutil.BadRequest("extsvcaccounts.ErrCannotCreateToken", errutil.WithPublicMessage("cannot add external service account token")) + + ErrCredentialsNotFound = errutil.NotFound("extsvcaccounts.credentials-not-found") ) // Credentials represents the credentials associated to an external service diff --git a/pkg/services/extsvcauth/extsvcaccounts/service.go b/pkg/services/serviceaccounts/extsvcaccounts/service.go similarity index 91% rename from pkg/services/extsvcauth/extsvcaccounts/service.go rename to pkg/services/serviceaccounts/extsvcaccounts/service.go index e8bb45cb535..6eed6b6e978 100644 --- a/pkg/services/extsvcauth/extsvcaccounts/service.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/service.go @@ -35,18 +35,18 @@ func ProvideExtSvcAccountsService(acSvc ac.Service, saSvc *manager.ServiceAccoun } // RetrieveExtSvcAccount fetches an external service account by ID -func (esa *ExtSvcAccountsService) RetrieveExtSvcAccount(ctx context.Context, orgID, saID int64) (*extsvcauth.ExtSvcAccount, error) { - sa, err := esa.saSvc.RetrieveServiceAccount(ctx, orgID, saID) +func (esa *ExtSvcAccountsService) RetrieveExtSvcAccount(ctx context.Context, orgID, saID int64) (*sa.ExtSvcAccount, error) { + svcAcc, err := esa.saSvc.RetrieveServiceAccount(ctx, orgID, saID) if err != nil { return nil, err } - return &extsvcauth.ExtSvcAccount{ - ID: sa.Id, - Login: sa.Login, - Name: sa.Name, - OrgID: sa.OrgId, - IsDisabled: sa.IsDisabled, - Role: roletype.RoleType(sa.Role), + return &sa.ExtSvcAccount{ + ID: svcAcc.Id, + Login: svcAcc.Login, + Name: svcAcc.Name, + OrgID: svcAcc.OrgId, + IsDisabled: svcAcc.IsDisabled, + Role: roletype.RoleType(svcAcc.Role), }, nil } @@ -63,7 +63,7 @@ func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd * esa.logger.Warn("Impersonation setup skipped. It is not possible to impersonate with a service account token.", "service", slug) } - saID, err := esa.ManageExtSvcAccount(ctx, &extsvcauth.ManageExtSvcAccountCmd{ + saID, err := esa.ManageExtSvcAccount(ctx, &sa.ManageExtSvcAccountCmd{ ExtSvcSlug: slug, Enabled: cmd.Self.Enabled, OrgID: extsvcauth.TmpOrgID, @@ -91,13 +91,13 @@ func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd * } // ManageExtSvcAccount creates, updates or deletes the service account associated with an external service -func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *extsvcauth.ManageExtSvcAccountCmd) (int64, error) { +func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *sa.ManageExtSvcAccountCmd) (int64, error) { if cmd == nil { esa.logger.Warn("Received no input") return 0, nil } - saID, errRetrieve := esa.saSvc.RetrieveServiceAccountIdByName(ctx, cmd.OrgID, ExtSvcPrefix+cmd.ExtSvcSlug) + saID, errRetrieve := esa.saSvc.RetrieveServiceAccountIdByName(ctx, cmd.OrgID, sa.ExtSvcPrefix+cmd.ExtSvcSlug) if errRetrieve != nil && !errors.Is(errRetrieve, sa.ErrServiceAccountNotFound) { return 0, errRetrieve } @@ -140,7 +140,7 @@ func (esa *ExtSvcAccountsService) saveExtSvcAccount(ctx context.Context, cmd *sa // Create a service account esa.logger.Debug("Create service account", "service", cmd.ExtSvcSlug, "orgID", cmd.OrgID) sa, err := esa.saSvc.CreateServiceAccount(ctx, cmd.OrgID, &sa.CreateServiceAccountForm{ - Name: ExtSvcPrefix + cmd.ExtSvcSlug, + Name: sa.ExtSvcPrefix + cmd.ExtSvcSlug, Role: newRole(roletype.RoleNone), IsDisabled: newBool(false), }) @@ -181,7 +181,7 @@ func (esa *ExtSvcAccountsService) deleteExtSvcAccount(ctx context.Context, orgID func (esa *ExtSvcAccountsService) getExtSvcAccountToken(ctx context.Context, orgID, saID int64, extSvcSlug string) (string, error) { // Get credentials from store credentials, err := esa.GetExtSvcCredentials(ctx, orgID, extSvcSlug) - if err != nil && !errors.Is(err, extsvcauth.ErrCredentialsNotFound) { + if err != nil && !errors.Is(err, ErrCredentialsNotFound) { return "", err } if credentials != nil { @@ -223,7 +223,7 @@ func (esa *ExtSvcAccountsService) GetExtSvcCredentials(ctx context.Context, orgI return nil, err } if !ok { - return nil, extsvcauth.ErrCredentialsNotFound.Errorf("No credential found for in store %v", extSvcSlug) + return nil, ErrCredentialsNotFound.Errorf("No credential found for in store %v", extSvcSlug) } return &Credentials{Secret: token}, nil } diff --git a/pkg/services/extsvcauth/extsvcaccounts/service_test.go b/pkg/services/serviceaccounts/extsvcaccounts/service_test.go similarity index 93% rename from pkg/services/extsvcauth/extsvcaccounts/service_test.go rename to pkg/services/serviceaccounts/extsvcaccounts/service_test.go index 3a5abbfe879..dc2034786bc 100644 --- a/pkg/services/extsvcauth/extsvcaccounts/service_test.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/service_test.go @@ -65,7 +65,7 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { tests := []struct { name string init func(env *TestEnv) - cmd extsvcauth.ManageExtSvcAccountCmd + cmd sa.ManageExtSvcAccountCmd checks func(t *testing.T, env *TestEnv) want int64 wantErr bool @@ -78,7 +78,7 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { env.SaSvc.On("DeleteServiceAccount", mock.Anything, mock.Anything, mock.Anything).Return(nil) env.AcStore.On("DeleteExternalServiceRole", mock.Anything, mock.Anything).Return(nil) }, - cmd: extsvcauth.ManageExtSvcAccountCmd{ + cmd: sa.ManageExtSvcAccountCmd{ ExtSvcSlug: extSvcSlug, Enabled: false, OrgID: extSvcOrgID, @@ -87,7 +87,7 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { checks: func(t *testing.T, env *TestEnv) { env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }), - mock.MatchedBy(func(slug string) bool { return slug == ExtSvcPrefix+extSvcSlug })) + mock.MatchedBy(func(slug string) bool { return slug == sa.ExtSvcPrefix+extSvcSlug })) env.SaSvc.AssertCalled(t, "DeleteServiceAccount", mock.Anything, mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }), mock.MatchedBy(func(saID int64) bool { return saID == extSvcAccID })) @@ -105,7 +105,7 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { env.SaSvc.On("DeleteServiceAccount", mock.Anything, mock.Anything, mock.Anything).Return(nil) env.AcStore.On("DeleteExternalServiceRole", mock.Anything, mock.Anything).Return(nil) }, - cmd: extsvcauth.ManageExtSvcAccountCmd{ + cmd: sa.ManageExtSvcAccountCmd{ ExtSvcSlug: extSvcSlug, Enabled: true, OrgID: extSvcOrgID, @@ -114,7 +114,7 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { checks: func(t *testing.T, env *TestEnv) { env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }), - mock.MatchedBy(func(slug string) bool { return slug == ExtSvcPrefix+extSvcSlug })) + mock.MatchedBy(func(slug string) bool { return slug == sa.ExtSvcPrefix+extSvcSlug })) env.SaSvc.AssertCalled(t, "DeleteServiceAccount", mock.Anything, mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }), mock.MatchedBy(func(saID int64) bool { return saID == extSvcAccID })) @@ -134,7 +134,7 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { Return(extSvcAccount, nil) env.AcStore.On("SaveExternalServiceRole", mock.Anything, mock.Anything).Return(nil) }, - cmd: extsvcauth.ManageExtSvcAccountCmd{ + cmd: sa.ManageExtSvcAccountCmd{ ExtSvcSlug: extSvcSlug, Enabled: true, OrgID: extSvcOrgID, @@ -143,11 +143,11 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { checks: func(t *testing.T, env *TestEnv) { env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }), - mock.MatchedBy(func(slug string) bool { return slug == ExtSvcPrefix+extSvcSlug })) + mock.MatchedBy(func(slug string) bool { return slug == sa.ExtSvcPrefix+extSvcSlug })) env.SaSvc.AssertCalled(t, "CreateServiceAccount", mock.Anything, mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }), mock.MatchedBy(func(cmd *sa.CreateServiceAccountForm) bool { - return cmd.Name == ExtSvcPrefix+extSvcSlug && *cmd.Role == roletype.RoleNone + return cmd.Name == sa.ExtSvcPrefix+extSvcSlug && *cmd.Role == roletype.RoleNone }), ) env.AcStore.AssertCalled(t, "SaveExternalServiceRole", mock.Anything, @@ -168,7 +168,7 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { Return(int64(11), nil) env.AcStore.On("SaveExternalServiceRole", mock.Anything, mock.Anything).Return(nil) }, - cmd: extsvcauth.ManageExtSvcAccountCmd{ + cmd: sa.ManageExtSvcAccountCmd{ ExtSvcSlug: extSvcSlug, Enabled: true, OrgID: extSvcOrgID, @@ -177,7 +177,7 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { checks: func(t *testing.T, env *TestEnv) { env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }), - mock.MatchedBy(func(slug string) bool { return slug == ExtSvcPrefix+extSvcSlug })) + mock.MatchedBy(func(slug string) bool { return slug == sa.ExtSvcPrefix+extSvcSlug })) env.AcStore.AssertCalled(t, "SaveExternalServiceRole", mock.Anything, mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool { return cmd.ServiceAccountID == int64(11) && cmd.ExternalServiceID == extSvcSlug && @@ -257,7 +257,7 @@ func TestExtSvcAccountsService_SaveExternalService(t *testing.T) { checks: func(t *testing.T, env *TestEnv) { env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), - mock.MatchedBy(func(slug string) bool { return slug == ExtSvcPrefix+extSvcSlug })) + mock.MatchedBy(func(slug string) bool { return slug == sa.ExtSvcPrefix+extSvcSlug })) env.SaSvc.AssertCalled(t, "DeleteServiceAccount", mock.Anything, mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), mock.MatchedBy(func(saID int64) bool { return saID == extSvcAccID })) @@ -287,7 +287,7 @@ func TestExtSvcAccountsService_SaveExternalService(t *testing.T) { checks: func(t *testing.T, env *TestEnv) { env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), - mock.MatchedBy(func(slug string) bool { return slug == ExtSvcPrefix+extSvcSlug })) + mock.MatchedBy(func(slug string) bool { return slug == sa.ExtSvcPrefix+extSvcSlug })) env.SaSvc.AssertCalled(t, "DeleteServiceAccount", mock.Anything, mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), mock.MatchedBy(func(saID int64) bool { return saID == extSvcAccID })) @@ -319,11 +319,11 @@ func TestExtSvcAccountsService_SaveExternalService(t *testing.T) { checks: func(t *testing.T, env *TestEnv) { env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), - mock.MatchedBy(func(slug string) bool { return slug == ExtSvcPrefix+extSvcSlug })) + mock.MatchedBy(func(slug string) bool { return slug == sa.ExtSvcPrefix+extSvcSlug })) env.SaSvc.AssertCalled(t, "CreateServiceAccount", mock.Anything, mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), mock.MatchedBy(func(cmd *sa.CreateServiceAccountForm) bool { - return cmd.Name == ExtSvcPrefix+extSvcSlug && *cmd.Role == roletype.RoleNone + return cmd.Name == sa.ExtSvcPrefix+extSvcSlug && *cmd.Role == roletype.RoleNone }), ) env.AcStore.AssertCalled(t, "SaveExternalServiceRole", mock.Anything, @@ -360,7 +360,7 @@ func TestExtSvcAccountsService_SaveExternalService(t *testing.T) { checks: func(t *testing.T, env *TestEnv) { env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), - mock.MatchedBy(func(slug string) bool { return slug == ExtSvcPrefix+extSvcSlug })) + mock.MatchedBy(func(slug string) bool { return slug == sa.ExtSvcPrefix+extSvcSlug })) env.AcStore.AssertCalled(t, "SaveExternalServiceRole", mock.Anything, mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool { return cmd.ServiceAccountID == int64(11) && cmd.ExternalServiceID == extSvcSlug && diff --git a/pkg/services/serviceaccounts/models.go b/pkg/services/serviceaccounts/models.go index a115628a148..6346cd6cd7c 100644 --- a/pkg/services/serviceaccounts/models.go +++ b/pkg/services/serviceaccounts/models.go @@ -3,6 +3,7 @@ package serviceaccounts import ( "time" + "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/org" @@ -16,6 +17,7 @@ var ( const ( ServiceAccountPrefix = "sa-" + ExtSvcPrefix = "extsvc-" ) const ( @@ -173,6 +175,23 @@ type Stats struct { ForcedExpiryEnabled bool `xorm:"-"` } +// ExtSvcAccount represents the service account associated to an external service +type ExtSvcAccount struct { + ID int64 + Login string + Name string + OrgID int64 + IsDisabled bool + Role roletype.RoleType +} + +type ManageExtSvcAccountCmd struct { + ExtSvcSlug string + Enabled bool // disabled: the service account and its permissions will be deleted + OrgID int64 + Permissions []accesscontrol.Permission +} + // AccessEvaluator is used to protect the "Configuration > Service accounts" page access var AccessEvaluator = accesscontrol.EvalAny( accesscontrol.EvalPermission(ActionRead), diff --git a/pkg/services/serviceaccounts/proxy/service.go b/pkg/services/serviceaccounts/proxy/service.go index 46785252a98..95e79cc1a28 100644 --- a/pkg/services/serviceaccounts/proxy/service.go +++ b/pkg/services/serviceaccounts/proxy/service.go @@ -6,8 +6,8 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apikey" - "github.com/grafana/grafana/pkg/services/extsvcauth/extsvcaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts" + "github.com/grafana/grafana/pkg/services/serviceaccounts/extsvcaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/manager" ) @@ -101,9 +101,9 @@ func (s *ServiceAccountsProxy) UpdateServiceAccount(ctx context.Context, orgID, } func isNameValid(name string) bool { - return !strings.HasPrefix(name, extsvcaccounts.ExtSvcPrefix) + return !strings.HasPrefix(name, serviceaccounts.ExtSvcPrefix) } func isExternalServiceAccount(login string) bool { - return strings.HasPrefix(login, serviceaccounts.ServiceAccountPrefix+extsvcaccounts.ExtSvcPrefix) + return strings.HasPrefix(login, serviceaccounts.ServiceAccountPrefix+serviceaccounts.ExtSvcPrefix) } diff --git a/pkg/services/serviceaccounts/proxy/service_test.go b/pkg/services/serviceaccounts/proxy/service_test.go index 78893503a5a..a1258b80369 100644 --- a/pkg/services/serviceaccounts/proxy/service_test.go +++ b/pkg/services/serviceaccounts/proxy/service_test.go @@ -6,8 +6,8 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apikey" - "github.com/grafana/grafana/pkg/services/extsvcauth/extsvcaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts" + "github.com/grafana/grafana/pkg/services/serviceaccounts/extsvcaccounts" "github.com/stretchr/testify/assert" ) diff --git a/pkg/services/serviceaccounts/serviceaccounts.go b/pkg/services/serviceaccounts/serviceaccounts.go index 5ef93b0fc1e..f7ab7e25b3f 100644 --- a/pkg/services/serviceaccounts/serviceaccounts.go +++ b/pkg/services/serviceaccounts/serviceaccounts.go @@ -22,3 +22,11 @@ type Service interface { AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *AddServiceAccountTokenCommand) (*apikey.APIKey, error) } + +//go:generate mockery --name ExtSvcAccountsService --structname MockExtSvcAccountsService --output tests --outpkg tests --filename extsvcaccmock.go +type ExtSvcAccountsService interface { + // ManageExtSvcAccount creates, updates or deletes the service account associated with an external service + ManageExtSvcAccount(ctx context.Context, cmd *ManageExtSvcAccountCmd) (int64, error) + // RetrieveExtSvcAccount fetches an external service account by ID + RetrieveExtSvcAccount(ctx context.Context, orgID, saID int64) (*ExtSvcAccount, error) +} diff --git a/pkg/services/extsvcauth/extsvcmocks/extsvcaccmock.go b/pkg/services/serviceaccounts/tests/extsvcaccmock.go similarity index 67% rename from pkg/services/extsvcauth/extsvcmocks/extsvcaccmock.go rename to pkg/services/serviceaccounts/tests/extsvcaccmock.go index b16fff68577..62dd0ef4abf 100644 --- a/pkg/services/extsvcauth/extsvcmocks/extsvcaccmock.go +++ b/pkg/services/serviceaccounts/tests/extsvcaccmock.go @@ -1,11 +1,11 @@ // Code generated by mockery v2.35.2. DO NOT EDIT. -package extsvcmocks +package tests import ( context "context" - extsvcauth "github.com/grafana/grafana/pkg/services/extsvcauth" + serviceaccounts "github.com/grafana/grafana/pkg/services/serviceaccounts" mock "github.com/stretchr/testify/mock" ) @@ -15,21 +15,21 @@ type MockExtSvcAccountsService struct { } // ManageExtSvcAccount provides a mock function with given fields: ctx, cmd -func (_m *MockExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *extsvcauth.ManageExtSvcAccountCmd) (int64, error) { +func (_m *MockExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *serviceaccounts.ManageExtSvcAccountCmd) (int64, error) { ret := _m.Called(ctx, cmd) var r0 int64 var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *extsvcauth.ManageExtSvcAccountCmd) (int64, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.ManageExtSvcAccountCmd) (int64, error)); ok { return rf(ctx, cmd) } - if rf, ok := ret.Get(0).(func(context.Context, *extsvcauth.ManageExtSvcAccountCmd) int64); ok { + if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.ManageExtSvcAccountCmd) int64); ok { r0 = rf(ctx, cmd) } else { r0 = ret.Get(0).(int64) } - if rf, ok := ret.Get(1).(func(context.Context, *extsvcauth.ManageExtSvcAccountCmd) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *serviceaccounts.ManageExtSvcAccountCmd) error); ok { r1 = rf(ctx, cmd) } else { r1 = ret.Error(1) @@ -39,19 +39,19 @@ func (_m *MockExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cm } // RetrieveExtSvcAccount provides a mock function with given fields: ctx, orgID, saID -func (_m *MockExtSvcAccountsService) RetrieveExtSvcAccount(ctx context.Context, orgID int64, saID int64) (*extsvcauth.ExtSvcAccount, error) { +func (_m *MockExtSvcAccountsService) RetrieveExtSvcAccount(ctx context.Context, orgID int64, saID int64) (*serviceaccounts.ExtSvcAccount, error) { ret := _m.Called(ctx, orgID, saID) - var r0 *extsvcauth.ExtSvcAccount + var r0 *serviceaccounts.ExtSvcAccount var r1 error - if rf, ok := ret.Get(0).(func(context.Context, int64, int64) (*extsvcauth.ExtSvcAccount, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, int64, int64) (*serviceaccounts.ExtSvcAccount, error)); ok { return rf(ctx, orgID, saID) } - if rf, ok := ret.Get(0).(func(context.Context, int64, int64) *extsvcauth.ExtSvcAccount); ok { + if rf, ok := ret.Get(0).(func(context.Context, int64, int64) *serviceaccounts.ExtSvcAccount); ok { r0 = rf(ctx, orgID, saID) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*extsvcauth.ExtSvcAccount) + r0 = ret.Get(0).(*serviceaccounts.ExtSvcAccount) } } From 0fc9da14229d96a6372c025112eb0cb6e032cf15 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Tue, 24 Oct 2023 10:15:38 +0100 Subject: [PATCH 009/180] Alerting: Improve documentation on high availability (#76434) --- .../fundamentals/high-availability/_index.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/sources/alerting/fundamentals/high-availability/_index.md b/docs/sources/alerting/fundamentals/high-availability/_index.md index 63c45a44b10..fe83d9d6446 100644 --- a/docs/sources/alerting/fundamentals/high-availability/_index.md +++ b/docs/sources/alerting/fundamentals/high-availability/_index.md @@ -21,20 +21,19 @@ weight: 430 # Alerting high availability -The Grafana Alerting system has two main components: a `Scheduler` and an internal `Alertmanager`. The `Scheduler` evaluates your alert rules, while the internal Alertmanager manages **routing** and **grouping**. - -When running Grafana Alerting in high availability, the operational mode of the scheduler remains unaffected, and each Grafana instance evaluates all alerts. The operational change happens in the Alertmanager when it deduplicates alert notifications across Grafana instances. +Grafana Alerting uses the Prometheus model of separating the evaluation of alert rules from the delivering of notifications. In this model the evaluation of alert rules is done in the alert generator and the delivering of notifications is done in the alert receiver. In Grafana Alerting, the alert generator is the Scheduler and the receiver is the Alertmanager. {{< figure src="/static/img/docs/alerting/unified/high-availability-ua.png" class="docs-image--no-shadow" max-width= "750px" caption="High availability" >}} -The coordination between Grafana instances happens via [a Gossip protocol](https://en.wikipedia.org/wiki/Gossip_protocol). Alerts are not gossiped between instances and each scheduler delivers the same volume of alerts to each Alertmanager. +When running multiple instances of Grafana, the operational mode of the alert generator does not change. This means that all alert rules are evaluated on all instances of Grafana. You can think of the evaluation of alert rules as being duplicated. However, this is how Grafana Alerting makes sure that as long as at least one Grafana instance is working, alert rules will still be evaluated and notifications for alerts will still be sent. You will see this duplication in state history, and is a good way to tell if you are using high availability. -The two types of messages gossiped between Grafana instances are: +While the alert generator evaluates all alert rules on all instances, Grafana makes a best-effort attempt to avoid sending duplicate notifications. Alertmanager chooses availability over consistency which means that in certain situations notifications can be duplicated or appear out-of-order. Alertmanager takes the opinion that duplicate or out-of-order notifications are better than no notifications, and so it uses a gossip protocol to share information about notifications between Grafana instances instead of a more-consistent but less-available protocol such as two-phase commit, or distributed-consensus protocols such as Raft or Paxos. -- Notification logs: Who (which instance) notified what (which alert). -- Silences: If an alert should fire or not. +The Alertmanager also gossips silences, which means a silence created on one Grafana instance is replicated to all other Grafana instances. -The notification logs and silences are persisted in the database periodically and during a graceful Grafana shut down. +Both notifications and silences are persisted to the database periodically, and during graceful shut down. + +It is important to make sure that gossiping is configured and tested. You can find the documentation on how to do that [here][configure-high-availability]. ## Useful links From 272a901e5ee1f1ec638bcb4c32f20b9134974c18 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Tue, 24 Oct 2023 10:50:19 +0100 Subject: [PATCH 010/180] Alerting: Improve order of docs pages (#76998) --- docs/sources/alerting/fundamentals/_index.md | 2 +- docs/sources/alerting/fundamentals/alert-rules/_index.md | 2 +- docs/sources/alerting/fundamentals/alertmanager.md | 2 +- docs/sources/alerting/fundamentals/annotation-label/_index.md | 2 +- docs/sources/alerting/fundamentals/contact-points/index.md | 2 +- docs/sources/alerting/fundamentals/evaluate-grafana-alerts.md | 2 +- docs/sources/alerting/fundamentals/high-availability/_index.md | 2 +- .../alerting/fundamentals/notification-policies/_index.md | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/sources/alerting/fundamentals/_index.md b/docs/sources/alerting/fundamentals/_index.md index 820c10e5b6e..da63a9465be 100644 --- a/docs/sources/alerting/fundamentals/_index.md +++ b/docs/sources/alerting/fundamentals/_index.md @@ -11,7 +11,7 @@ labels: - oss menuTitle: Introduction title: Introduction to Alerting -weight: 105 +weight: 150 --- # Introduction to Alerting diff --git a/docs/sources/alerting/fundamentals/alert-rules/_index.md b/docs/sources/alerting/fundamentals/alert-rules/_index.md index 9b279325757..d5b0c84a11c 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/_index.md +++ b/docs/sources/alerting/fundamentals/alert-rules/_index.md @@ -11,7 +11,7 @@ labels: - enterprise - oss title: Alert rules -weight: 106 +weight: 130 --- # Alert rules diff --git a/docs/sources/alerting/fundamentals/alertmanager.md b/docs/sources/alerting/fundamentals/alertmanager.md index 8094fb7db2e..f3a29067c36 100644 --- a/docs/sources/alerting/fundamentals/alertmanager.md +++ b/docs/sources/alerting/fundamentals/alertmanager.md @@ -12,7 +12,7 @@ labels: - enterprise - oss title: Alertmanager -weight: 103 +weight: 140 --- # Alertmanager diff --git a/docs/sources/alerting/fundamentals/annotation-label/_index.md b/docs/sources/alerting/fundamentals/annotation-label/_index.md index be929a30aeb..e689ea10d2f 100644 --- a/docs/sources/alerting/fundamentals/annotation-label/_index.md +++ b/docs/sources/alerting/fundamentals/annotation-label/_index.md @@ -16,7 +16,7 @@ labels: - enterprise - oss title: Labels and annotations -weight: 110 +weight: 120 --- # Labels and annotations diff --git a/docs/sources/alerting/fundamentals/contact-points/index.md b/docs/sources/alerting/fundamentals/contact-points/index.md index 3c28c0c5df5..3d90b43bcec 100644 --- a/docs/sources/alerting/fundamentals/contact-points/index.md +++ b/docs/sources/alerting/fundamentals/contact-points/index.md @@ -18,7 +18,7 @@ labels: - enterprise - oss title: Contact points -weight: 106 +weight: 150 --- # Contact points diff --git a/docs/sources/alerting/fundamentals/evaluate-grafana-alerts.md b/docs/sources/alerting/fundamentals/evaluate-grafana-alerts.md index 8ba0b4b3e86..84dccd52633 100644 --- a/docs/sources/alerting/fundamentals/evaluate-grafana-alerts.md +++ b/docs/sources/alerting/fundamentals/evaluate-grafana-alerts.md @@ -10,7 +10,7 @@ labels: - enterprise - oss title: Alerting on numeric data -weight: 116 +weight: 110 --- # Alerting on numeric data diff --git a/docs/sources/alerting/fundamentals/high-availability/_index.md b/docs/sources/alerting/fundamentals/high-availability/_index.md index fe83d9d6446..93f2356c3d4 100644 --- a/docs/sources/alerting/fundamentals/high-availability/_index.md +++ b/docs/sources/alerting/fundamentals/high-availability/_index.md @@ -16,7 +16,7 @@ labels: - enterprise - oss title: Alerting high availability -weight: 430 +weight: 170 --- # Alerting high availability diff --git a/docs/sources/alerting/fundamentals/notification-policies/_index.md b/docs/sources/alerting/fundamentals/notification-policies/_index.md index 57e11835124..2a32f951cfb 100644 --- a/docs/sources/alerting/fundamentals/notification-policies/_index.md +++ b/docs/sources/alerting/fundamentals/notification-policies/_index.md @@ -11,7 +11,7 @@ labels: - enterprise - oss title: Notifications -weight: 107 +weight: 160 --- # Notifications From ced065c7e9e46c4f28217d47b7ce0d15085b73d4 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 24 Oct 2023 11:53:22 +0100 Subject: [PATCH 011/180] Chore: fix some more types (#76535) * clean up some e2e/runtime types * fix some stories * some more fixes * fix route props * update unit tests * update more unit tests * don't throw here --- .betterer.results | 86 ++----------------- e2e/cypress/support/index.d.ts | 2 +- e2e/utils/flows/addDataSource.ts | 4 +- e2e/utils/support/scenarioContext.ts | 5 +- e2e/utils/support/types.ts | 8 +- packages/grafana-runtime/src/services/live.ts | 2 +- .../src/utils/DataSourceWithBackend.test.ts | 22 +++++ .../src/utils/DataSourceWithBackend.ts | 11 ++- packages/grafana-runtime/src/utils/plugin.ts | 4 +- .../src/utils/queryResponse.test.ts | 6 +- .../src/utils/queryResponse.ts | 9 +- .../src/components/Dropdown/ButtonSelect.tsx | 3 + .../src/components/Forms/FieldArray.story.tsx | 6 +- .../JSONFormatter/JSONFormatter.tsx | 2 +- .../src/components/Layout/Layout.story.tsx | 6 +- .../src/components/Menu/MenuGroup.tsx | 2 +- .../src/components/Menu/MenuItem.tsx | 2 +- .../src/components/Modal/ModalsContext.tsx | 2 +- .../src/components/QueryField/QueryField.tsx | 6 +- .../components/Segment/SegmentAsync.story.tsx | 7 +- .../src/components/ThemeDemos/ThemeDemo.tsx | 4 +- .../src/components/TimeSeries/TimeSeries.tsx | 6 +- .../core/navigation/__mocks__/routeProps.ts | 5 +- .../influxdb/datasource_backend_mode.test.ts | 5 +- 24 files changed, 90 insertions(+), 125 deletions(-) diff --git a/.betterer.results b/.betterer.results index 8a0dc3b9dfc..b5af32e93b2 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5,24 +5,8 @@ // exports[`better eslint`] = { value: `{ - "e2e/cypress/support/index.d.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "e2e/utils/flows/addDataSource.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "e2e/utils/support/scenarioContext.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "e2e/utils/support/types.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Do not use any type assertions.", "3"], - [0, 0, 0, "Do not use any type assertions.", "4"], - [0, 0, 0, "Do not use any type assertions.", "5"], - [0, 0, 0, "Do not use any type assertions.", "6"], - [0, 0, 0, "Do not use any type assertions.", "7"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "packages/grafana-data/src/dataframe/ArrayDataFrame.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -732,31 +716,15 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "11"], [0, 0, 0, "Unexpected any. Specify a different type.", "12"] ], - "packages/grafana-runtime/src/services/live.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "packages/grafana-runtime/src/utils/DataSourceWithBackend.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"] - ], - "packages/grafana-runtime/src/utils/plugin.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/grafana-runtime/src/utils/queryResponse.test.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], - "packages/grafana-runtime/src/utils/queryResponse.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Do not use any type assertions.", "3"], - [0, 0, 0, "Do not use any type assertions.", "4"] + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"] + ], + "packages/grafana-runtime/src/utils/queryResponse.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] ], "packages/grafana-schema/src/veneer/common.types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] @@ -816,12 +784,6 @@ exports[`better eslint`] = { "packages/grafana-ui/src/components/Drawer/Drawer.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] ], - "packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "packages/grafana-ui/src/components/Forms/FieldArray.story.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "packages/grafana-ui/src/components/Forms/Legacy/Input/Input.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], @@ -871,28 +833,16 @@ exports[`better eslint`] = { "packages/grafana-ui/src/components/InfoBox/InfoBox.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "packages/grafana-ui/src/components/JSONFormatter/json_explorer/json_explorer.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "packages/grafana-ui/src/components/Layout/Layout.story.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "packages/grafana-ui/src/components/MatchersUI/FieldValueMatcher.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], "packages/grafana-ui/src/components/MatchersUI/fieldMatchersUI.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "packages/grafana-ui/src/components/Menu/MenuGroup.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/grafana-ui/src/components/Menu/MenuItem.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "packages/grafana-ui/src/components/Menu/SubMenu.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"], [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"] @@ -901,9 +851,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"] + [0, 0, 0, "Unexpected any. Specify a different type.", "3"] ], "packages/grafana-ui/src/components/Monaco/CodeEditor.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] @@ -922,11 +870,7 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"] ], "packages/grafana-ui/src/components/QueryField/QueryField.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"] - ], - "packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] ], "packages/grafana-ui/src/components/Segment/SegmentSelect.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -1040,14 +984,6 @@ exports[`better eslint`] = { "packages/grafana-ui/src/components/Tags/Tag.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], - "packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] - ], "packages/grafana-ui/src/components/TimeSeries/utils.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], @@ -1556,10 +1492,6 @@ exports[`better eslint`] = { "public/app/core/navigation/GrafanaRouteError.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], - "public/app/core/navigation/__mocks__/routeProps.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], "public/app/core/navigation/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/e2e/cypress/support/index.d.ts b/e2e/cypress/support/index.d.ts index d01547041ff..40dcf83105e 100644 --- a/e2e/cypress/support/index.d.ts +++ b/e2e/cypress/support/index.d.ts @@ -8,7 +8,7 @@ interface CompareScreenshotsConfig { declare namespace Cypress { interface Chainable { compareScreenshots(config: CompareScreenshotsConfig | string): Chainable; - logToConsole(message: string, optional?: any): void; + logToConsole(message: string, optional?: unknown): void; readProvisions(filePaths: string[]): Chainable; getJSONFilesFromDir(dirPath: string): Chainable; startBenchmarking(testName: string): void; diff --git a/e2e/utils/flows/addDataSource.ts b/e2e/utils/flows/addDataSource.ts index bdc0cc453f5..777ed6caf31 100644 --- a/e2e/utils/flows/addDataSource.ts +++ b/e2e/utils/flows/addDataSource.ts @@ -2,8 +2,6 @@ import { v4 as uuidv4 } from 'uuid'; import { e2e } from '../index'; -import { DeleteDataSourceConfig } from './deleteDataSource'; - export interface AddDataSourceConfig { basicAuth: boolean; basicAuthPassword: string; @@ -96,7 +94,7 @@ export const addDataSource = (config?: Partial) => { return cy.url().then(() => { e2e.getScenarioContext().then(({ addedDataSources }) => { e2e.setScenarioContext({ - addedDataSources: [...addedDataSources, { name } as DeleteDataSourceConfig], + addedDataSources: [...addedDataSources, { name, id: '' }], }); }); diff --git a/e2e/utils/support/scenarioContext.ts b/e2e/utils/support/scenarioContext.ts index eeb09ced3c4..15fcc01ed32 100644 --- a/e2e/utils/support/scenarioContext.ts +++ b/e2e/utils/support/scenarioContext.ts @@ -9,7 +9,6 @@ export interface ScenarioContext { lastAddedDataSource: string; // @todo rename to `lastAddedDataSourceName` lastAddedDataSourceId: string; hasChangedUserPreferences: boolean; - [key: string]: any; } const scenarioContext: ScenarioContext = { @@ -50,9 +49,7 @@ export const setScenarioContext = (newContext: Partial): Cypres .wrap( { setScenarioContext: () => { - Object.entries(newContext).forEach(([key, value]) => { - scenarioContext[key] = value; - }); + Object.assign(scenarioContext, newContext); }, }, { log: false } diff --git a/e2e/utils/support/types.ts b/e2e/utils/support/types.ts index d498dc68bea..193ad190b24 100644 --- a/e2e/utils/support/types.ts +++ b/e2e/utils/support/types.ts @@ -17,7 +17,7 @@ export type TypeSelectors = S extends StringSelector ? E2EFunction : S extends UrlSelector ? E2EVisit & Omit, 'url'> - : S extends Record + : S extends Record ? E2EFunctions : S; @@ -32,7 +32,7 @@ export type E2EFactoryArgs = { selectors: S }; export type CypressOptions = Partial; const processSelectors = (e2eObjects: E2EFunctions, selectors: S): E2EFunctions => { - const logOutput = (data: any) => cy.logToConsole('Retrieving Selector:', data); + const logOutput = (data: unknown) => cy.logToConsole('Retrieving Selector:', data); const keys = Object.keys(selectors); for (let index = 0; index < keys.length; index++) { const key = keys[index]; @@ -80,7 +80,7 @@ const processSelectors = (e2eObjects: E2EFunctions, sele e2eObjects[key] = function (textOrOptions?: string | CypressOptions, options?: CypressOptions) { // the input can only be () if (arguments.length === 0) { - const selector = value(undefined as unknown as string); + const selector = value(''); logOutput(selector); return cy.get(selector); @@ -97,7 +97,7 @@ const processSelectors = (e2eObjects: E2EFunctions, sele logOutput(selector); return cy.get(selector); } - const selector = value(undefined as unknown as string); + const selector = value(''); logOutput(selector); return cy.get(selector, textOrOptions); diff --git a/packages/grafana-runtime/src/services/live.ts b/packages/grafana-runtime/src/services/live.ts index 7d38ff8720e..e3787383995 100644 --- a/packages/grafana-runtime/src/services/live.ts +++ b/packages/grafana-runtime/src/services/live.ts @@ -79,7 +79,7 @@ export interface GrafanaLiveSrv { * * @alpha -- experimental */ - publish(address: LiveChannelAddress, data: unknown): Promise; + publish(address: LiveChannelAddress, data: unknown): Promise; } let singletonInstance: GrafanaLiveSrv; diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts index c521e5c3633..b1815fcc095 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts @@ -11,6 +11,7 @@ import { createDataFrame, AdHocVariableFilter, ScopedVars, + getDefaultTimeRange, } from '@grafana/data'; import { config } from '../config'; @@ -61,6 +62,15 @@ jest.mock('../services', () => ({ jest.mock('./publicDashboardQueryHandler'); describe('DataSourceWithBackend', () => { + beforeEach(async () => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2023-10-13')); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + test('check the executed queries', () => { const { mock, ds } = createMockDatasource(); ds.query({ @@ -70,6 +80,7 @@ describe('DataSourceWithBackend', () => { dashboardUID: 'dashA', panelId: 123, filters: [{ key: 'key1', operator: '=', value: 'val1' }], + range: getDefaultTimeRange(), queryGroupId: 'abc', } as DataQueryRequest); @@ -79,6 +90,7 @@ describe('DataSourceWithBackend', () => { expect(args).toMatchInlineSnapshot(` { "data": { + "from": "1697133600000", "queries": [ { "applyTemplateVariablesCalled": true, @@ -111,6 +123,7 @@ describe('DataSourceWithBackend', () => { "refId": "B", }, ], + "to": "1697155200000", }, "headers": { "X-Dashboard-Uid": "dashA", @@ -135,6 +148,7 @@ describe('DataSourceWithBackend', () => { targets: [{ refId: 'A' }, { refId: 'B', datasource: { type: '__expr__' } }], dashboardUID: 'dashA', panelId: 123, + range: getDefaultTimeRange(), queryGroupId: 'abc', } as DataQueryRequest); @@ -144,6 +158,7 @@ describe('DataSourceWithBackend', () => { expect(args).toMatchInlineSnapshot(` { "data": { + "from": "1697133600000", "queries": [ { "applyTemplateVariablesCalled": true, @@ -167,6 +182,7 @@ describe('DataSourceWithBackend', () => { "refId": "B", }, ], + "to": "1697155200000", }, "headers": { "X-Dashboard-Uid": "dashA", @@ -190,6 +206,7 @@ describe('DataSourceWithBackend', () => { ds.query({ maxDataPoints: 10, intervalMs: 5000, + range: getDefaultTimeRange(), targets: [{ refId: 'A' }, { refId: 'B', datasource: { type: 'sample' } }], } as DataQueryRequest); @@ -205,6 +222,7 @@ describe('DataSourceWithBackend', () => { targets: [{ refId: 'A' }, { refId: 'B', datasource: { type: 'sample' } }], hideFromInspector: true, dashboardUID: 'dashA', + range: getDefaultTimeRange(), panelId: 123, } as DataQueryRequest); @@ -214,6 +232,7 @@ describe('DataSourceWithBackend', () => { expect(args).toMatchInlineSnapshot(` { "data": { + "from": "1697133600000", "queries": [ { "applyTemplateVariablesCalled": true, @@ -240,6 +259,7 @@ describe('DataSourceWithBackend', () => { "refId": "B", }, ], + "to": "1697155200000", }, "headers": { "X-Dashboard-Uid": "dashA", @@ -353,6 +373,7 @@ describe('DataSourceWithBackend', () => { dashboardUID: 'dashA', panelId: 123, queryGroupId: 'abc', + range: getDefaultTimeRange(), } as DataQueryRequest; ds.query(request); @@ -371,6 +392,7 @@ describe('DataSourceWithBackend', () => { dashboardUID: 'dashA', panelId: 123, queryGroupId: 'abc', + range: getDefaultTimeRange(), } as DataQueryRequest; ds.query(request); diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts index 0eea64b8899..da8a914251c 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts @@ -190,12 +190,11 @@ class DataSourceWithBackend< return of({ data: [] }); } - const body: any = { queries }; - - if (range) { - body.from = range.from.valueOf().toString(); - body.to = range.to.valueOf().toString(); - } + const body = { + queries, + from: range?.from.valueOf().toString(), + to: range?.to.valueOf().toString(), + }; if (config.featureToggles.queryOverLive) { return getGrafanaLiveSrv().getQueryData({ diff --git a/packages/grafana-runtime/src/utils/plugin.ts b/packages/grafana-runtime/src/utils/plugin.ts index 8f11c6f811e..ce2ab40b5b3 100644 --- a/packages/grafana-runtime/src/utils/plugin.ts +++ b/packages/grafana-runtime/src/utils/plugin.ts @@ -31,10 +31,10 @@ export const SystemJS = window.System; * @param options - plugin styling for light and dark theme. * @public */ -export async function loadPluginCss(options: PluginCssOptions): Promise { +export async function loadPluginCss(options: PluginCssOptions): Promise { try { const cssPath = config.bootData.user.theme === 'light' ? options.light : options.dark; - return await SystemJS.import(cssPath); + return SystemJS.import(cssPath); } catch (err) { console.error(err); } diff --git a/packages/grafana-runtime/src/utils/queryResponse.test.ts b/packages/grafana-runtime/src/utils/queryResponse.test.ts index adec09b9f9d..4b978d3615b 100644 --- a/packages/grafana-runtime/src/utils/queryResponse.test.ts +++ b/packages/grafana-runtime/src/utils/queryResponse.test.ts @@ -262,13 +262,13 @@ describe('Query Response parser', () => { data: { results: { X: { - series: [{ name: 'Requests/s', points: [[13.594958983547151, 1611839862951]] }] as any, + series: [{ target: '', datapoints: [[13.594958983547151, 1611839862951]] }], }, B: { - series: [{ name: 'Requests/s', points: [[13.594958983547151, 1611839862951]] }] as any, + series: [{ target: '', datapoints: [[13.594958983547151, 1611839862951]] }], }, A: { - series: [{ name: 'Requests/s', points: [[13.594958983547151, 1611839862951]] }] as any, + series: [{ target: '', datapoints: [[13.594958983547151, 1611839862951]] }], }, }, }, diff --git a/packages/grafana-runtime/src/utils/queryResponse.ts b/packages/grafana-runtime/src/utils/queryResponse.ts index 19b0d4b9e7f..0b434a69f8a 100644 --- a/packages/grafana-runtime/src/utils/queryResponse.ts +++ b/packages/grafana-runtime/src/utils/queryResponse.ts @@ -73,10 +73,11 @@ export function toDataQueryResponse( } // If the response isn't in a correct shape we just ignore the data and pass empty DataQueryResponse. - if ((res as FetchResponse).data?.results) { - const results = (res as FetchResponse).data.results; + const fetchResponse = res as FetchResponse; + if (fetchResponse.data?.results) { + const results = fetchResponse.data.results; const refIDs = queries?.length ? queries.map((q) => q.refId) : Object.keys(results); - const cachedResponse = isCachedResponse(res as FetchResponse); + const cachedResponse = isCachedResponse(fetchResponse); const data: DataResponse[] = []; for (const refId of refIDs) { @@ -144,7 +145,7 @@ export function toDataQueryResponse( } // When it is not an OK response, make sure the error gets added - if ((res as FetchResponse).status && (res as FetchResponse).status !== 200) { + if (fetchResponse.status && fetchResponse.status !== 200) { if (rsp.state !== LoadingState.Error) { rsp.state = LoadingState.Error; } diff --git a/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx index a6dba1060bb..12bc9d9afe9 100644 --- a/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx +++ b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx @@ -89,6 +89,9 @@ const ButtonSelectComponent = (props: Props) => { ButtonSelectComponent.displayName = 'ButtonSelect'; +// needed to properly forward the generic type through React.memo +// see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/37087#issuecomment-656596623 +// eslint-disable-next-line @typescript-eslint/consistent-type-assertions export const ButtonSelect = React.memo(ButtonSelectComponent) as typeof ButtonSelectComponent; const getStyles = (theme: GrafanaTheme2) => { diff --git a/packages/grafana-ui/src/components/Forms/FieldArray.story.tsx b/packages/grafana-ui/src/components/Forms/FieldArray.story.tsx index 3a5f134d76b..08a5dea278b 100644 --- a/packages/grafana-ui/src/components/Forms/FieldArray.story.tsx +++ b/packages/grafana-ui/src/components/Forms/FieldArray.story.tsx @@ -9,7 +9,7 @@ import { withStoryContainer } from '../../utils/storybook/withStoryContainer'; import { FieldArray } from './FieldArray'; import mdx from './FieldArray.mdx'; -export default { +const meta: Meta = { title: 'Forms/FieldArray', component: FieldArray, decorators: [withStoryContainer], @@ -25,7 +25,9 @@ export default { containerWidth: { control: { type: 'range', min: 100, max: 500, step: 10 } }, containerHeight: { control: { type: 'range', min: 100, max: 500, step: 10 } }, }, -} as Meta; +}; + +export default meta; export const Simple: Story = (args) => { const defaultValues: FieldValues = { diff --git a/packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx b/packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx index d7b89a29a2a..5a0bb78c353 100644 --- a/packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx +++ b/packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx @@ -7,7 +7,7 @@ interface Props { json: {}; config?: JsonExplorerConfig; open?: number; - onDidRender?: (formattedJson: any) => void; + onDidRender?: (formattedJson: {}) => void; } export class JSONFormatter extends PureComponent { diff --git a/packages/grafana-ui/src/components/Layout/Layout.story.tsx b/packages/grafana-ui/src/components/Layout/Layout.story.tsx index 52d87e40964..1391eb3467a 100644 --- a/packages/grafana-ui/src/components/Layout/Layout.story.tsx +++ b/packages/grafana-ui/src/components/Layout/Layout.story.tsx @@ -8,7 +8,7 @@ import { withStoryContainer } from '../../utils/storybook/withStoryContainer'; import { Layout, LayoutProps } from './Layout'; import mdx from './Layout.mdx'; -export default { +const meta: Meta = { title: 'Layout/Groups', component: Layout, decorators: [withStoryContainer], @@ -56,7 +56,9 @@ export default { }, }, }, -} as Meta; +}; + +export default meta; export const Horizontal: Story = (args) => { return ( diff --git a/packages/grafana-ui/src/components/Menu/MenuGroup.tsx b/packages/grafana-ui/src/components/Menu/MenuGroup.tsx index 305ba6b353a..4241dd2e608 100644 --- a/packages/grafana-ui/src/components/Menu/MenuGroup.tsx +++ b/packages/grafana-ui/src/components/Menu/MenuGroup.tsx @@ -9,7 +9,7 @@ import { useStyles2 } from '../../themes'; import { MenuItemProps } from './MenuItem'; /** @internal */ -export interface MenuItemsGroup { +export interface MenuItemsGroup { /** Label for the menu items group */ label?: string; /** Aria label for accessibility support */ diff --git a/packages/grafana-ui/src/components/Menu/MenuItem.tsx b/packages/grafana-ui/src/components/Menu/MenuItem.tsx index 0b8033b4f5c..74ef322bd43 100644 --- a/packages/grafana-ui/src/components/Menu/MenuItem.tsx +++ b/packages/grafana-ui/src/components/Menu/MenuItem.tsx @@ -14,7 +14,7 @@ import { SubMenu } from './SubMenu'; export type MenuItemElement = HTMLAnchorElement & HTMLButtonElement & HTMLDivElement; /** @internal */ -export interface MenuItemProps { +export interface MenuItemProps { /** Label of the menu item */ label: string; /** Aria label for accessibility support */ diff --git a/packages/grafana-ui/src/components/Modal/ModalsContext.tsx b/packages/grafana-ui/src/components/Modal/ModalsContext.tsx index 274d10b85f2..d2841532942 100644 --- a/packages/grafana-ui/src/components/Modal/ModalsContext.tsx +++ b/packages/grafana-ui/src/components/Modal/ModalsContext.tsx @@ -33,7 +33,7 @@ export class ModalsProvider extends Component, props: any) => { + showModal = (component: React.ComponentType, props: T) => { this.setState({ component, props, diff --git a/packages/grafana-ui/src/components/QueryField/QueryField.tsx b/packages/grafana-ui/src/components/QueryField/QueryField.tsx index cfb9ef7c5ac..040e265df46 100644 --- a/packages/grafana-ui/src/components/QueryField/QueryField.tsx +++ b/packages/grafana-ui/src/components/QueryField/QueryField.tsx @@ -1,7 +1,7 @@ import { css, cx } from '@emotion/css'; import classnames from 'classnames'; import { debounce } from 'lodash'; -import React, { Context, PureComponent } from 'react'; +import React, { PureComponent } from 'react'; import { Value } from 'slate'; import Plain from 'slate-plain-serializer'; import { Editor, EventHook, Plugin } from 'slate-react'; @@ -67,8 +67,8 @@ export class UnThemedQueryField extends PureComponent) { - super(props, context); + constructor(props: QueryFieldProps) { + super(props); this.runOnChangeDebounced = debounce(this.runOnChange, 500); diff --git a/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx b/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx index 99fd5e3a37e..918a5af232f 100644 --- a/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx +++ b/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx @@ -22,7 +22,12 @@ const loadOptions = (options: T): Promise => new Promise((res) => setTime const loadOptionsErr = (): Promise>> => new Promise((_, rej) => setTimeout(() => rej(Error('Could not find data')), 2000)); -const SegmentFrame = ({ loadOptions, children }: any) => ( +const SegmentFrame = ({ + loadOptions, + children, +}: React.PropsWithChildren<{ + loadOptions: (options: Array>) => Promise>>; +}>) => ( <> {children} diff --git a/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx b/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx index 377f14beba3..ccfea2e4dff 100644 --- a/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx +++ b/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx @@ -174,8 +174,8 @@ export const ThemeDemo = () => { - {Object.keys(t.shadows).map((key) => ( - + {Object.entries(t.shadows).map(([key, value]) => ( + ))} diff --git a/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx b/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx index 849b7b38656..019671e2b6a 100644 --- a/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx +++ b/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx @@ -4,7 +4,7 @@ import { DataFrame, TimeRange } from '@grafana/data'; import { withTheme2 } from '../../themes/ThemeContext'; import { GraphNG, GraphNGProps, PropDiffFn } from '../GraphNG/GraphNG'; -import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext'; +import { PanelContextRoot } from '../PanelChrome/PanelContext'; import { hasVisibleLegendSeries, PlotLegend } from '../uPlot/PlotLegend'; import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; @@ -16,10 +16,10 @@ type TimeSeriesProps = Omit { static contextType = PanelContextRoot; - panelContext: PanelContext = {} as PanelContext; + declare context: React.ContextType; prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { - const { eventBus, eventsScope, sync } = this.context as PanelContext; + const { eventBus, eventsScope, sync } = this.context; const { theme, timeZone, renderers, tweakAxis, tweakScale } = this.props; return preparePlotConfigBuilder({ diff --git a/public/app/core/navigation/__mocks__/routeProps.ts b/public/app/core/navigation/__mocks__/routeProps.ts index 23eac8bf7ad..b4437ef2d71 100644 --- a/public/app/core/navigation/__mocks__/routeProps.ts +++ b/public/app/core/navigation/__mocks__/routeProps.ts @@ -1,5 +1,6 @@ import { createMemoryHistory } from 'history'; import { merge } from 'lodash'; +import { match } from 'react-router-dom'; import { GrafanaRouteComponentProps } from '../types'; @@ -14,12 +15,12 @@ export function getRouteComponentProps, route: { path: '', component: () => null, }, - queryParams: {} as any, + queryParams: {} as Q, }; return merge(overrides, defaults); diff --git a/public/app/plugins/datasource/influxdb/datasource_backend_mode.test.ts b/public/app/plugins/datasource/influxdb/datasource_backend_mode.test.ts index c1e5d5f0b64..de3c2d3248a 100644 --- a/public/app/plugins/datasource/influxdb/datasource_backend_mode.test.ts +++ b/public/app/plugins/datasource/influxdb/datasource_backend_mode.test.ts @@ -130,7 +130,7 @@ describe('InfluxDataSource Backend Mode', () => { ...queryOptions, targets: [...queryOptions.targets, { ...influxQuery, adhocFilters }], }; - await ctx.ds.query(req); + ctx.ds.query(req); }); it('should add adhocFilters to the tags in the query', () => { @@ -244,6 +244,7 @@ describe('InfluxDataSource Backend Mode', () => { it('should render chained regex variables with floating point number', () => { ds.metricFindQuery(`SELECT sum("piece_count") FROM "rp"."pdata" WHERE diameter <= $maxSED`, { + ...queryOptions, scopedVars: { maxSED: { text: '8.1', value: '8.1' } }, }); const qe = `SELECT sum("piece_count") FROM "rp"."pdata" WHERE diameter <= 8.1`; @@ -253,6 +254,7 @@ describe('InfluxDataSource Backend Mode', () => { it('should render chained regex variables with URL', () => { ds.metricFindQuery('SHOW TAG VALUES WITH KEY = "agent_url" WHERE agent_url =~ /^$var1$/', { + ...queryOptions, scopedVars: { var1: { text: 'https://aaaa-aa-aaa.bbb.ccc.ddd:8443/ggggg', @@ -269,6 +271,7 @@ describe('InfluxDataSource Backend Mode', () => { ds.metricFindQuery( 'SELECT sum("piece_count") FROM "rp"."pdata" WHERE diameter <= $maxSED AND agent_url =~ /^$var1$/', { + ...queryOptions, scopedVars: { var1: { text: 'https://aaaa-aa-aaa.bbb.ccc.ddd:8443/ggggg', From 575981201c379b8e2b03474578972886ab75b6b5 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Tue, 24 Oct 2023 13:58:14 +0300 Subject: [PATCH 012/180] revert table tracking (#77021) --- .../panel/table/TableCellOptionEditor.tsx | 4 ---- public/app/plugins/panel/table/TablePanel.tsx | 19 +------------------ 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/public/app/plugins/panel/table/TableCellOptionEditor.tsx b/public/app/plugins/panel/table/TableCellOptionEditor.tsx index 5eb8b698b36..f03957ff0a8 100644 --- a/public/app/plugins/panel/table/TableCellOptionEditor.tsx +++ b/public/app/plugins/panel/table/TableCellOptionEditor.tsx @@ -3,11 +3,9 @@ import { merge } from 'lodash'; import React, { useState } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { reportInteraction } from '@grafana/runtime'; import { TableCellOptions } from '@grafana/schema'; import { Field, Select, TableCellDisplayMode, useStyles2 } from '@grafana/ui'; -import { INTERACTION_EVENT_NAME, INTERACTION_ITEM } from './TablePanel'; import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor'; import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor'; import { SparklineCellOptionsEditor } from './cells/SparklineCellOptionsEditor'; @@ -44,8 +42,6 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => { value = merge(value, settingCache[value.type]); } - reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.CELL_TYPE_CHANGE, type: value.type }); - onChange(value); } }; diff --git a/public/app/plugins/panel/table/TablePanel.tsx b/public/app/plugins/panel/table/TablePanel.tsx index ff7771bd291..d9d4e85ec40 100644 --- a/public/app/plugins/panel/table/TablePanel.tsx +++ b/public/app/plugins/panel/table/TablePanel.tsx @@ -2,22 +2,13 @@ import { css } from '@emotion/css'; import React from 'react'; import { DataFrame, FieldMatcherID, getFrameDisplayName, PanelProps, SelectableValue } from '@grafana/data'; -import { PanelDataErrorView, reportInteraction } from '@grafana/runtime'; +import { PanelDataErrorView } from '@grafana/runtime'; import { Select, Table, usePanelContext, useTheme2 } from '@grafana/ui'; import { TableSortByFieldState } from '@grafana/ui/src/components/Table/types'; import { hasDeprecatedParentRowIndex, migrateFromParentRowIndexToNestedFrames } from './migrations'; import { Options } from './panelcfg.gen'; -export const INTERACTION_EVENT_NAME = 'table_panel_usage'; -export const INTERACTION_ITEM = { - COLUMN_RESIZE: 'column_resize', - SORT_BY: 'sort_by', - TABLE_SELECTION_CHANGE: 'table_selection_change', - ERROR_VIEW: 'error_view', - CELL_TYPE_CHANGE: 'cell_type_change', -}; - interface Props extends PanelProps {} export function TablePanel(props: Props) { @@ -36,8 +27,6 @@ export function TablePanel(props: Props) { let tableHeight = height; if (!count || !hasFields) { - reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.ERROR_VIEW }); - return ; } @@ -117,8 +106,6 @@ function onColumnResize(fieldDisplayName: string, width: number, props: Props) { }); } - reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.COLUMN_RESIZE }); - props.onFieldConfigChange({ ...fieldConfig, overrides, @@ -126,8 +113,6 @@ function onColumnResize(fieldDisplayName: string, width: number, props: Props) { } function onSortByChange(sortBy: TableSortByFieldState[], props: Props) { - reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.SORT_BY }); - props.onOptionsChange({ ...props.options, sortBy, @@ -135,8 +120,6 @@ function onSortByChange(sortBy: TableSortByFieldState[], props: Props) { } function onChangeTableSelection(val: SelectableValue, props: Props) { - reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.TABLE_SELECTION_CHANGE }); - props.onOptionsChange({ ...props.options, frameIndex: val.value || 0, From cad3c43bb193304a71ba9ebd44c1cd90dd4652e8 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 24 Oct 2023 13:06:18 +0200 Subject: [PATCH 013/180] Team LBAC: Move middleware to enterprise (#76969) * Team LBAC: Move middleware to enterprise * Remove ds proxy part * Move utils to enterprise --- pkg/api/pluginproxy/ds_proxy.go | 9 -- .../team_headers_middleware.go | 131 ------------------ .../pluginsintegration/pluginsintegration.go | 8 +- pkg/util/proxyutil/proxyutil.go | 63 --------- pkg/util/proxyutil/proxyutil_test.go | 94 ------------- 5 files changed, 2 insertions(+), 303 deletions(-) delete mode 100644 pkg/services/pluginsintegration/clientmiddleware/team_headers_middleware.go diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index 10afcf13bc2..35c1ca2efc5 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -272,15 +272,6 @@ func (proxy *DataSourceProxy) director(req *http.Request) { if proxy.features.IsEnabled(featuremgmt.FlagIdForwarding) { proxyutil.ApplyForwardIDHeader(req, proxy.ctx.SignedInUser) } - - if proxy.features.IsEnabled(featuremgmt.FlagTeamHttpHeaders) { - err := proxyutil.ApplyTeamHTTPHeaders(req, proxy.ds, proxy.ctx.Teams) - if err != nil { - // NOTE: could downgrade the errors to warnings - ctxLogger.Error("Error applying teamHTTPHeaders", "error", err) - return - } - } } func (proxy *DataSourceProxy) validateRequest() error { diff --git a/pkg/services/pluginsintegration/clientmiddleware/team_headers_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/team_headers_middleware.go deleted file mode 100644 index 9cb10240785..00000000000 --- a/pkg/services/pluginsintegration/clientmiddleware/team_headers_middleware.go +++ /dev/null @@ -1,131 +0,0 @@ -package clientmiddleware - -import ( - "context" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/infra/appcontext" - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/services/contexthandler" - "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/util/proxyutil" -) - -// NewTeamHTTPHeaderMiddleware creates a new plugins.ClientMiddleware that will -// set headers based on teams user is member of. -func NewTeamHTTPHeadersMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { - return &TeamHTTPHeadersMiddleware{ - next: next, - } - }) -} - -type TeamHTTPHeadersMiddleware struct { - next plugins.Client -} - -func (m *TeamHTTPHeadersMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - if req == nil { - return m.next.QueryData(ctx, req) - } - - err := m.setHeaders(ctx, req.PluginContext, req) - if err != nil { - return nil, err - } - - return m.next.QueryData(ctx, req) -} - -func (m *TeamHTTPHeadersMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - if req == nil { - return m.next.CallResource(ctx, req, sender) - } - - err := m.setHeaders(ctx, req.PluginContext, req) - if err != nil { - return err - } - - return m.next.CallResource(ctx, req, sender) -} - -func (m *TeamHTTPHeadersMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - if req == nil { - return m.next.CheckHealth(ctx, req) - } - - // NOTE: might not be needed to set headers. we want to for now set these headers - err := m.setHeaders(ctx, req.PluginContext, req) - if err != nil { - return nil, err - } - - return m.next.CheckHealth(ctx, req) -} - -func (m *TeamHTTPHeadersMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { - return m.next.CollectMetrics(ctx, req) -} - -func (m *TeamHTTPHeadersMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { - return m.next.SubscribeStream(ctx, req) -} - -func (m *TeamHTTPHeadersMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { - return m.next.PublishStream(ctx, req) -} - -func (m *TeamHTTPHeadersMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { - return m.next.RunStream(ctx, req, sender) -} - -func (m *TeamHTTPHeadersMiddleware) setHeaders(ctx context.Context, pCtx backend.PluginContext, req interface{}) error { - reqCtx := contexthandler.FromContext(ctx) - // if request not for a datasource or no HTTP request context skip middleware - if req == nil || pCtx.DataSourceInstanceSettings == nil || reqCtx == nil || reqCtx.Req == nil { - return nil - } - - settings := pCtx.DataSourceInstanceSettings - jsonDataBytes, err := simplejson.NewJson(settings.JSONData) - if err != nil { - return err - } - - ds := &datasources.DataSource{ - ID: settings.ID, - OrgID: pCtx.OrgID, - JsonData: jsonDataBytes, - Updated: settings.Updated, - } - - signedInUser, err := appcontext.User(ctx) - if err != nil { - return nil // no user - } - - teamHTTPHeaders, err := proxyutil.GetTeamHTTPHeaders(ds, signedInUser.GetTeams()) - if err != nil { - return err - } - - switch t := req.(type) { - case *backend.QueryDataRequest: - for key, value := range teamHTTPHeaders { - t.SetHTTPHeader(key, value) - } - case *backend.CheckHealthRequest: - for key, value := range teamHTTPHeaders { - t.SetHTTPHeader(key, value) - } - case *backend.CallResourceRequest: - for key, value := range teamHTTPHeaders { - t.SetHTTPHeader(key, value) - } - } - - return nil -} diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index 9a89255fffd..8fd1143ad08 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -60,8 +60,6 @@ var WireSet = wire.NewSet( wire.Bind(new(plugins.RendererManager), new(*pluginstore.Service)), wire.Bind(new(plugins.SecretsPluginManager), new(*pluginstore.Service)), wire.Bind(new(plugins.StaticRouteResolver), new(*pluginstore.Service)), - ProvideClientDecorator, - wire.Bind(new(plugins.Client), new(*client.Decorator)), process.ProvideService, wire.Bind(new(process.Manager), new(*process.Service)), coreplugin.ProvideCoreRegistry, @@ -127,6 +125,8 @@ var WireExtensionSet = wire.NewSet( wire.Bind(new(plugins.PluginLoaderAuthorizer), new(*signature.UnsignedPluginAuthorizer)), wire.Bind(new(finder.Finder), new(*finder.Local)), finder.ProvideLocalFinder, + ProvideClientDecorator, + wire.Bind(new(plugins.Client), new(*client.Decorator)), ) func ProvideClientDecorator( @@ -179,10 +179,6 @@ func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthToken middlewares = append(middlewares, clientmiddleware.NewUserHeaderMiddleware()) } - if features.IsEnabled(featuremgmt.FlagTeamHttpHeaders) { - middlewares = append(middlewares, clientmiddleware.NewTeamHTTPHeadersMiddleware()) - } - middlewares = append(middlewares, clientmiddleware.NewHTTPClientMiddleware()) return middlewares diff --git a/pkg/util/proxyutil/proxyutil.go b/pkg/util/proxyutil/proxyutil.go index 43183d07800..0677fa5a396 100644 --- a/pkg/util/proxyutil/proxyutil.go +++ b/pkg/util/proxyutil/proxyutil.go @@ -4,13 +4,10 @@ import ( "fmt" "net" "net/http" - "net/url" "sort" - "strconv" "strings" "github.com/grafana/grafana/pkg/services/auth/identity" - "github.com/grafana/grafana/pkg/services/datasources" ) const ( @@ -133,63 +130,3 @@ func ApplyForwardIDHeader(req *http.Request, user identity.Requester) { req.Header.Set(IDHeaderName, token) } } - -func ApplyTeamHTTPHeaders(req *http.Request, ds *datasources.DataSource, teams []int64) error { - headers, err := GetTeamHTTPHeaders(ds, teams) - if err != nil { - return err - } - - for header, value := range headers { - // check if headerv is already set in req.Header - if req.Header.Get(header) != "" { - req.Header.Add(header, value) - } else { - req.Header.Set(header, value) - } - } - - return nil -} - -func GetTeamHTTPHeaders(ds *datasources.DataSource, teams []int64) (map[string]string, error) { - teamHTTPHeadersMap := make(map[string]string) - teamHTTPHeaders, err := ds.TeamHTTPHeaders() - if err != nil { - return nil, err - } - - for teamID, headers := range teamHTTPHeaders { - id, err := strconv.ParseInt(teamID, 10, 64) - if err != nil { - // FIXME: logging here - continue - } - - if !contains(teams, id) { - continue - } - - for _, header := range headers { - // Header values should be properly escaped. - if value, ok := teamHTTPHeadersMap[header.Header]; ok { - // Add multiple header values as a comma-separated strings according to RFC 7230 - // https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6 - teamHTTPHeadersMap[header.Header] = fmt.Sprintf("%s,%s", value, url.PathEscape(header.Value)) - } else { - teamHTTPHeadersMap[header.Header] = url.PathEscape(header.Value) - } - } - } - - return teamHTTPHeadersMap, nil -} - -func contains(slice []int64, value int64) bool { - for _, v := range slice { - if v == value { - return true - } - } - return false -} diff --git a/pkg/util/proxyutil/proxyutil_test.go b/pkg/util/proxyutil/proxyutil_test.go index e05b295df26..4ea59ee61ea 100644 --- a/pkg/util/proxyutil/proxyutil_test.go +++ b/pkg/util/proxyutil/proxyutil_test.go @@ -6,8 +6,6 @@ import ( "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/user" ) @@ -205,95 +203,3 @@ func TestApplyUserHeader(t *testing.T) { require.Equal(t, "admin", req.Header.Get("X-Grafana-User")) }) } - -func TestApplyteamHTTPHeaders(t *testing.T) { - testCases := []struct { - desc string - jsonData any - userTeams []int64 - want map[string]string - }{ - { - desc: "Should apply team headers for users teams", - jsonData: map[string]interface{}{ - "1": []map[string]interface{}{ - { - "header": "X-Team-Header", - "value": "1", - }, - }, - "2": []map[string]interface{}{ - { - "header": "X-Prom-Label-Policy", - "value": "2", - }, - }, - // user is not part of this team - "3": []map[string]interface{}{ - { - "header": "X-Custom-Label-Policy", - "value": "3", - }, - }, - }, - userTeams: []int64{1, 2}, - want: map[string]string{ - "X-Team-Header": "1", - "X-Prom-Label-Policy": "2", - }, - }, - { - desc: "Should be able to parse header values with commas", - jsonData: map[string]interface{}{ - "101": []map[string]interface{}{ - { - "header": "X-Prom-Label-Policy", - "value": `1234:{ foo="bar", bar="baz" }`, - }, - }, - }, - userTeams: []int64{101}, - want: map[string]string{ - "X-Prom-Label-Policy": "1234:%7B%20foo=%22bar%22%2C%20bar=%22baz%22%20%7D", - }, - }, { - desc: "Should be able to handle multiple header values", - jsonData: map[string]interface{}{ - "101": []map[string]interface{}{ - { - "header": "X-Prom-Label-Policy", - "value": `1234:{ foo="bar" }`, - }, - { - "header": "X-Prom-Label-Policy", - "value": `1234:{ bar="baz" }`, - }, - }, - }, - userTeams: []int64{101}, - want: map[string]string{ - "X-Prom-Label-Policy": "1234:%7B%20foo=%22bar%22%20%7D,1234:%7B%20bar=%22baz%22%20%7D", - }, - }, - } - - for _, testCase := range testCases { - t.Run("Should apply team headers for users teams", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, "/", nil) - require.NoError(t, err) - ds := &datasources.DataSource{ - JsonData: simplejson.New(), - } - - // add team headers - ds.JsonData.Set("teamHttpHeaders", testCase.jsonData) - - err = ApplyTeamHTTPHeaders(req, ds, testCase.userTeams) - require.NoError(t, err) - for header, value := range testCase.want { - require.Contains(t, req.Header, header) - require.Equal(t, value, req.Header.Get(header)) - } - }) - } -} From 07cc7504ee95120d619befcdf596ef45aa5213f1 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 24 Oct 2023 12:16:32 +0100 Subject: [PATCH 014/180] Icon: Simplify and remove wrapping `
` (#76819) * remove wrapping div * update tests, just gotta figure out how to handle fontawesome :( * add spinner.svg which matches the font-awesome spinner * add mock for react-inlinesvg * update mock and fix some tests * fix FormField test * fix CorrelationsPage tests * increase timeout --- jest.config.js | 1 + packages/grafana-data/src/types/icon.ts | 2 + .../DataLinksListItem.test.tsx | 2 - .../DateTimePickers/TimeRangeInput.tsx | 2 +- .../components/FormField/FormField.test.tsx | 6 +- .../src/components/FormLabel/FormLabel.tsx | 2 +- .../grafana-ui/src/components/Icon/Icon.tsx | 69 ++++++++----------- .../RefreshPicker/RefreshPicker.tsx | 2 +- .../src/components/Spinner/Spinner.tsx | 2 +- .../core/components/TagFilter/TagBadge.tsx | 2 +- .../components/receivers/GlobalConfigForm.tsx | 2 +- .../components/receivers/TemplateForm.tsx | 2 +- .../receivers/form/ChannelSubForm.tsx | 2 +- .../receivers/form/ReceiverForm.tsx | 2 +- .../QueryAndExpressionsStep.tsx | 2 +- .../components/silences/SilencesEditor.tsx | 2 +- .../correlations/CorrelationsPage.test.tsx | 30 +++++--- .../Forms/CorrelationFormNavigation.tsx | 2 +- .../EmbeddedDashboard/SaveDashboardForm.tsx | 2 +- .../SaveDashboard/forms/SaveDashboardForm.tsx | 2 +- public/app/features/playlist/PlaylistForm.tsx | 7 +- .../page/components/MoveToFolderModal.tsx | 2 +- .../VisualMetricQueryEditor.test.tsx | 4 +- .../VariableQueryEditor.test.tsx.snap | 8 ++- .../components/QueryEditor/QueryHeader.tsx | 2 +- .../LogGroups/LogGroupsSelector.test.tsx | 2 +- .../loki/components/LokiQueryEditor.tsx | 2 +- .../components/PromQueryEditorSelector.tsx | 2 +- public/img/icons/unicons/spinner-alt.svg | 1 - public/img/icons/unicons/spinner.svg | 4 +- public/test/mocks/react-inlinesvg.tsx | 7 ++ 31 files changed, 95 insertions(+), 84 deletions(-) delete mode 100644 public/img/icons/unicons/spinner-alt.svg create mode 100644 public/test/mocks/react-inlinesvg.tsx diff --git a/jest.config.js b/jest.config.js index 7f49f0d8d00..3370925ec1d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -28,6 +28,7 @@ module.exports = { moduleNameMapper: { '\\.svg': '/public/test/mocks/svg.ts', '\\.css': '/public/test/mocks/style.ts', + 'react-inlinesvg': '/public/test/mocks/react-inlinesvg.tsx', 'monaco-editor/esm/vs/editor/editor.api': '/public/test/mocks/monaco.ts', // near-membrane-dom won't work in a nodejs environment. '@locker/near-membrane-dom': '/public/test/mocks/nearMembraneDom.ts', diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index d2e9d9303bf..b75521182db 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -93,6 +93,7 @@ export const availableIconsIndex = { eye: true, 'eye-slash': true, 'ellipsis-h': true, + /* @deprecated, use 'spinner' instead */ 'fa fa-spinner': true, favorite: true, 'file-alt': true, @@ -198,6 +199,7 @@ export const availableIconsIndex = { sitemap: true, slack: true, 'sliders-v-alt': true, + spinner: true, 'sort-amount-down': true, 'sort-amount-up': true, 'square-shape': true, diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx index b0877f8bcba..90df2a95099 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx @@ -84,7 +84,6 @@ describe('DataLinksListItem', () => { setupTestContext({ link }); expect(screen.getByText(/data link url not provided/i)).toBeInTheDocument(); - expect(screen.getByTitle('')).toBeInTheDocument(); }); }); @@ -109,7 +108,6 @@ describe('DataLinksListItem', () => { setupTestContext({ link }); expect(screen.getByText(/data link url not provided/i)).toBeInTheDocument(); - expect(screen.getByTitle('')).toBeInTheDocument(); }); }); }); diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx index 1e60b37a3e1..d0eb37aa84e 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx @@ -68,7 +68,7 @@ export const TimeRangeInput = ({ onChange(timeRange); }; - const onRangeClear = (event: MouseEvent) => { + const onRangeClear = (event: MouseEvent) => { event.stopPropagation(); const from = dateTime(null); const to = dateTime(null); diff --git a/packages/grafana-ui/src/components/FormField/FormField.test.tsx b/packages/grafana-ui/src/components/FormField/FormField.test.tsx index 553d92e0e17..c64baf0c568 100644 --- a/packages/grafana-ui/src/components/FormField/FormField.test.tsx +++ b/packages/grafana-ui/src/components/FormField/FormField.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; @@ -42,8 +42,6 @@ describe('FormField', () => { screen.getAllByRole('textbox')[0].focus(); await userEvent.tab(); - await waitFor(() => { - screen.getByText(tooltip); - }); + expect(await screen.findByText(tooltip)).toBeInTheDocument(); }); }); diff --git a/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx b/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx index f413e444bc9..bd5e24fdc6c 100644 --- a/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx +++ b/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx @@ -37,7 +37,7 @@ export const FormLabel = ({ {children} {tooltip && ( - + )} diff --git a/packages/grafana-ui/src/components/Icon/Icon.tsx b/packages/grafana-ui/src/components/Icon/Icon.tsx index ab1feb1c055..8920290a259 100644 --- a/packages/grafana-ui/src/components/Icon/Icon.tsx +++ b/packages/grafana-ui/src/components/Icon/Icon.tsx @@ -9,7 +9,7 @@ import { IconName, IconType, IconSize } from '../../types/icon'; import { getIconRoot, getIconSubDir, getSvgSize } from './utils'; -export interface IconProps extends React.HTMLAttributes { +export interface IconProps extends Omit, 'onLoad' | 'onError' | 'ref'> { name: IconName; size?: IconSize; type?: IconType; @@ -18,16 +18,14 @@ export interface IconProps extends React.HTMLAttributes { const getIconStyles = (theme: GrafanaTheme2) => { return { - // line-height: 0; is needed for correct icon alignment in Safari - container: css({ - label: 'Icon', - display: 'inline-block', - lineHeight: 0, - }), icon: css({ - verticalAlign: 'middle', display: 'inline-block', fill: 'currentColor', + flexShrink: 0, + label: 'Icon', + // line-height: 0; is needed for correct icon alignment in Safari + lineHeight: 0, + verticalAlign: 'middle', }), orange: css({ fill: theme.v1.palette.orange, @@ -35,53 +33,44 @@ const getIconStyles = (theme: GrafanaTheme2) => { }; }; -export const Icon = React.forwardRef( - ({ size = 'md', type = 'default', name, className, style, title = '', ...divElementProps }, ref) => { +export const Icon = React.forwardRef( + ({ size = 'md', type = 'default', name, className, style, title = '', ...rest }, ref) => { const styles = useStyles2(getIconStyles); - /* Temporary solution to display also font awesome icons */ - if (name?.startsWith('fa fa-')) { - return ; - } - if (!isIconName(name)) { console.warn('Icon component passed an invalid icon name', name); } - if (!name || name.includes('..')) { - return
invalid icon name
; - } + // handle the deprecated 'fa fa-spinner' + const iconName: IconName = name === 'fa fa-spinner' ? 'spinner' : name; const iconRoot = getIconRoot(); const svgSize = getSvgSize(size); const svgHgt = svgSize; const svgWid = name.startsWith('gf-bar-align') ? 16 : name.startsWith('gf-interp') ? 30 : svgSize; - const subDir = getIconSubDir(name, type); - const svgPath = `${iconRoot}${subDir}/${name}.svg`; + const subDir = getIconSubDir(iconName, type); + const svgPath = `${iconRoot}${subDir}/${iconName}.svg`; return ( -
- -
+ ); } ); Icon.displayName = 'Icon'; - -function getFontAwesomeIconStyles(iconName: string, className?: string): string { - return cx( - iconName, - { - 'fa-spin': iconName === 'fa fa-spinner', - }, - className - ); -} diff --git a/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx b/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx index ab08d0d51ef..1ea9e8e2163 100644 --- a/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx +++ b/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx @@ -112,7 +112,7 @@ export class RefreshPicker extends PureComponent { tooltip={tooltip} onClick={onRefresh} variant={variant} - icon={isLoading ? 'fa fa-spinner' : 'sync'} + icon={isLoading ? 'spinner' : 'sync'} style={width ? { width } : undefined} data-testid={selectors.components.RefreshPicker.runButtonV2} > diff --git a/packages/grafana-ui/src/components/Spinner/Spinner.tsx b/packages/grafana-ui/src/components/Spinner/Spinner.tsx index b27a4675b47..a6a68d5cccc 100644 --- a/packages/grafana-ui/src/components/Spinner/Spinner.tsx +++ b/packages/grafana-ui/src/components/Spinner/Spinner.tsx @@ -28,7 +28,7 @@ export const Spinner = ({ className, inline = false, iconClassName, style, size const styles = getStyles(size, inline); return (
- +
); }; diff --git a/public/app/core/components/TagFilter/TagBadge.tsx b/public/app/core/components/TagFilter/TagBadge.tsx index 8d4c84502d2..59c83c72b8c 100644 --- a/public/app/core/components/TagFilter/TagBadge.tsx +++ b/public/app/core/components/TagFilter/TagBadge.tsx @@ -6,7 +6,7 @@ export interface Props { label: string; removeIcon: boolean; count: number; - onClick?: React.MouseEventHandler; + onClick?: React.MouseEventHandler; } export class TagBadge extends React.Component { diff --git a/public/app/features/alerting/unified/components/receivers/GlobalConfigForm.tsx b/public/app/features/alerting/unified/components/receivers/GlobalConfigForm.tsx index 33c5da50700..e0c4975a04d 100644 --- a/public/app/features/alerting/unified/components/receivers/GlobalConfigForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/GlobalConfigForm.tsx @@ -91,7 +91,7 @@ export const GlobalConfigForm = ({ config, alertManagerSourceName }: Props) => { {!readOnly && ( <> {loading && ( - )} diff --git a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx index c498d25bd2b..2fa2eb4f66c 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx @@ -203,7 +203,7 @@ export const TemplateForm = ({ existing, alertManagerSourceName, config, provena
{loading && ( - )} diff --git a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx index 77691f9ecbb..8fd3f9056a1 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx @@ -159,7 +159,7 @@ export function ChannelSubForm({ variant="secondary" type="button" onClick={() => handleTest()} - icon={testingReceiver ? 'fa fa-spinner' : 'message'} + icon={testingReceiver ? 'spinner' : 'message'} > Test diff --git a/public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx index a301e3573e5..5f0476e9456 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx @@ -186,7 +186,7 @@ export function ReceiverForm({ {isEditable && ( <> {isSubmitting && ( - )} diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx index 6b2b2febf60..b1b56b46e48 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx @@ -488,7 +488,7 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P {config.expressionsEnabled && } {isPreviewLoading && ( - )} diff --git a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx index 93f08924190..ebcefaced79 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx @@ -242,7 +242,7 @@ export const SilencesEditor = ({ silence, alertManagerSourceName }: Props) => {
{loading && ( - )} diff --git a/public/app/features/correlations/CorrelationsPage.test.tsx b/public/app/features/correlations/CorrelationsPage.test.tsx index dc5e8f18a17..478b13ee1b7 100644 --- a/public/app/features/correlations/CorrelationsPage.test.tsx +++ b/public/app/features/correlations/CorrelationsPage.test.tsx @@ -292,7 +292,9 @@ describe('CorrelationsPage', () => { await userEvent.click(await screen.findByRole('button', { name: /add$/i })); - expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_added'); + await waitFor(() => { + expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_added'); + }); // the table showing correlations should have appeared expect(await screen.findByRole('table')).toBeInTheDocument(); @@ -439,7 +441,9 @@ describe('CorrelationsPage', () => { await userEvent.click(screen.getByRole('button', { name: /add$/i })); - expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_added'); + await waitFor(() => { + expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_added'); + }); // the table showing correlations should have appeared expect(await screen.findByRole('table')).toBeInTheDocument(); @@ -474,7 +478,9 @@ describe('CorrelationsPage', () => { expect(screen.queryByRole('cell', { name: /some label$/i })).not.toBeInTheDocument(); - expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_deleted'); + await waitFor(() => { + expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_deleted'); + }); }); it('correctly edits correlations', async () => { @@ -486,7 +492,9 @@ describe('CorrelationsPage', () => { const rowExpanderButton = within(tableRows[0]).getByRole('button', { name: /toggle row expanded/i }); await userEvent.click(rowExpanderButton); - expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_details_expanded'); + await waitFor(() => { + expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_details_expanded'); + }); await userEvent.clear(screen.getByRole('textbox', { name: /label/i })); await userEvent.type(screen.getByRole('textbox', { name: /label/i }), 'edited label'); @@ -500,9 +508,11 @@ describe('CorrelationsPage', () => { await userEvent.click(screen.getByRole('button', { name: /save$/i })); - expect(await screen.findByRole('cell', { name: /edited label$/i })).toBeInTheDocument(); + expect(await screen.findByRole('cell', { name: /edited label$/i }, { timeout: 5000 })).toBeInTheDocument(); - expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_edited'); + await waitFor(() => { + expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_edited'); + }); }); it('correctly edits transformations', async () => { @@ -553,7 +563,9 @@ describe('CorrelationsPage', () => { expect(screen.getByText('Please define an expression')).toBeInTheDocument(); await userEvent.type(screen.getByLabelText(/expression/i), 'test expression'); await userEvent.click(screen.getByRole('button', { name: /save$/i })); - expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_edited'); + await waitFor(() => { + expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_edited'); + }); }); }); @@ -684,7 +696,9 @@ describe('CorrelationsPage', () => { await userEvent.click(rowExpanderButton); - expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_details_expanded'); + await waitFor(() => { + expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_details_expanded'); + }); // form elements should be readonly const labelInput = await screen.findByRole('textbox', { name: /label/i }); diff --git a/public/app/features/correlations/Forms/CorrelationFormNavigation.tsx b/public/app/features/correlations/Forms/CorrelationFormNavigation.tsx index 287fe2564ca..fb08aa8d671 100644 --- a/public/app/features/correlations/Forms/CorrelationFormNavigation.tsx +++ b/public/app/features/correlations/Forms/CorrelationFormNavigation.tsx @@ -11,7 +11,7 @@ export const CorrelationFormNavigation = () => { const { readOnly, loading, correlation } = useCorrelationsFormContext(); const LastPageNext = !readOnly && ( - ); diff --git a/public/app/features/dashboard/components/EmbeddedDashboard/SaveDashboardForm.tsx b/public/app/features/dashboard/components/EmbeddedDashboard/SaveDashboardForm.tsx index 910486e18a0..63f41ba0000 100644 --- a/public/app/features/dashboard/components/EmbeddedDashboard/SaveDashboardForm.tsx +++ b/public/app/features/dashboard/components/EmbeddedDashboard/SaveDashboardForm.tsx @@ -45,7 +45,7 @@ export const SaveDashboardForm = ({ dashboard, onCancel, onSubmit, onSuccess, sa - {!hasChanges &&
No changes to save
} diff --git a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx index 92ac378d9e3..622eef95c90 100644 --- a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx @@ -126,7 +126,7 @@ export const SaveDashboardForm = ({
- diff --git a/public/app/features/search/page/components/MoveToFolderModal.tsx b/public/app/features/search/page/components/MoveToFolderModal.tsx index 31440bc7c2d..e0451bf6e6c 100644 --- a/public/app/features/search/page/components/MoveToFolderModal.tsx +++ b/public/app/features/search/page/components/MoveToFolderModal.tsx @@ -132,7 +132,7 @@ export const MoveToFolderModal = ({ results, onMoveItems, onDismiss }: Props) => - diff --git a/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.test.tsx index 841e5d58e3c..a1f785be110 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.test.tsx @@ -138,7 +138,9 @@ describe('VisualMetricQueryEditor', () => { expect(screen.getByText('metric.test_label')).toBeInTheDocument(); const service = await screen.findByLabelText('Service'); openMenu(service); - await select(service, 'Srv 2', { container: document.body }); + await act(async () => { + await select(service, 'Srv 2', { container: document.body }); + }); expect(onChange).toBeCalledWith(expect.objectContaining({ filters: ['metric.type', '=', 'type2'] })); expect(query).toEqual(defaultQuery); expect(screen.queryByText('metric.test_label')).not.toBeInTheDocument(); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/__snapshots__/VariableQueryEditor.test.tsx.snap b/public/app/plugins/datasource/cloud-monitoring/components/__snapshots__/VariableQueryEditor.test.tsx.snap index 8d61351c259..dd402db3302 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/__snapshots__/VariableQueryEditor.test.tsx.snap +++ b/public/app/plugins/datasource/cloud-monitoring/components/__snapshots__/VariableQueryEditor.test.tsx.snap @@ -88,8 +88,12 @@ exports[`VariableQueryEditor renders correctly 1`] = `
-
diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryHeader.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryHeader.tsx index d697131a905..4e0dff5e540 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryHeader.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryHeader.tsx @@ -107,7 +107,7 @@ const QueryHeader = ({ variant={dataIsStale ? 'primary' : 'secondary'} size="sm" onClick={onRunQuery} - icon={data?.state === LoadingState.Loading ? 'fa fa-spinner' : undefined} + icon={data?.state === LoadingState.Loading ? 'spinner' : undefined} disabled={data?.state === LoadingState.Loading || emptyLogsExpression} > Run queries diff --git a/public/app/plugins/datasource/cloudwatch/components/shared/LogGroups/LogGroupsSelector.test.tsx b/public/app/plugins/datasource/cloudwatch/components/shared/LogGroups/LogGroupsSelector.test.tsx index 3d89c73ba0e..4f69b4dc33e 100644 --- a/public/app/plugins/datasource/cloudwatch/components/shared/LogGroups/LogGroupsSelector.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/shared/LogGroups/LogGroupsSelector.test.tsx @@ -285,7 +285,7 @@ describe('LogGroupsSelector', () => { /> ); await userEvent.click(screen.getByText('Select log groups')); - await screen.getByRole('button', { name: 'select-clear-value' }).click(); + await userEvent.click(screen.getByRole('button', { name: 'select-clear-value' })); await userEvent.click(screen.getByText('Add log groups')); expect(onChange).toHaveBeenCalledWith([ { diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx index eeb814cc25f..a9293b17fa8 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx @@ -176,7 +176,7 @@ export const LokiQueryEditor = React.memo((props) => { variant={dataIsStale ? 'primary' : 'secondary'} size="sm" onClick={onRunQuery} - icon={data?.state === LoadingState.Loading ? 'fa fa-spinner' : undefined} + icon={data?.state === LoadingState.Loading ? 'spinner' : undefined} disabled={data?.state === LoadingState.Loading} > {queries && queries.length > 1 ? `Run queries` : `Run query`} diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx index 63108bfea33..d222406603d 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx @@ -130,7 +130,7 @@ export const PromQueryEditorSelector = React.memo((props) => { variant={dataIsStale ? 'primary' : 'secondary'} size="sm" onClick={onRunQuery} - icon={data?.state === LoadingState.Loading ? 'fa fa-spinner' : undefined} + icon={data?.state === LoadingState.Loading ? 'spinner' : undefined} disabled={data?.state === LoadingState.Loading} > Run queries diff --git a/public/img/icons/unicons/spinner-alt.svg b/public/img/icons/unicons/spinner-alt.svg deleted file mode 100644 index d197db9dfeb..00000000000 --- a/public/img/icons/unicons/spinner-alt.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/img/icons/unicons/spinner.svg b/public/img/icons/unicons/spinner.svg index dbc7f48a55c..fca18c39393 100644 --- a/public/img/icons/unicons/spinner.svg +++ b/public/img/icons/unicons/spinner.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/public/test/mocks/react-inlinesvg.tsx b/public/test/mocks/react-inlinesvg.tsx new file mode 100644 index 00000000000..b0326a7a6fe --- /dev/null +++ b/public/test/mocks/react-inlinesvg.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export default function ReactInlineSVG({ src, innerRef, cacheRequests, preProcessor, ...rest }) { + return ; +} + +export const cacheStore = {}; From 1cb1d174fda71a2b4792b066b833cd7207278131 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Tue, 24 Oct 2023 12:41:08 +0100 Subject: [PATCH 015/180] Alerting: Fix confusion around what can and cannot be customized in notifications (#77032) * Alerting: Fix confusion around what can and cannot be customized in notifications * Small fix * Second small fix --- .../template-notifications/_index.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/sources/alerting/manage-notifications/template-notifications/_index.md b/docs/sources/alerting/manage-notifications/template-notifications/_index.md index 40d347fc73c..dffd7502fc9 100644 --- a/docs/sources/alerting/manage-notifications/template-notifications/_index.md +++ b/docs/sources/alerting/manage-notifications/template-notifications/_index.md @@ -25,15 +25,17 @@ Notification templates are not tied to specific contact point integrations, such You can use notification templates to: -- Add, remove, or re-order information in the notification including the summary, description, labels and annotations, values, and links -- Format text in bold and italic, and add or remove line breaks +- Customize the subject of an email or the title of a message. +- Add, change or remove text in notifications. For example, to select or omit certain labels, annotations and links. +- Format text in bold and italic, and add or remove line breaks. You cannot use notification templates to: -- Change how images are included in notifications, such as the number of images in each notification or where in the notification inline images are shown -- Change the design of notifications in instant messaging services such as Slack and Microsoft Teams -- Change the data in webhook notifications, including the structure of the JSON request or sending data in other formats such as XML -- Add or remove HTTP headers in webhook notifications other than those in the contact point configuration +- Add HTML and CSS to email notifications to change their visual appearance. +- Change the design of notifications in instant messaging services such as Slack and Microsoft Teams. For example, to add or remove custom blocks with Slack Block Kit or adaptive cards with Microsoft Teams. +- Choose the number and size of images, or where in the notification images are shown. +- Customize the data in webhooks, including the fields or structure of the JSON data or send the data in other formats such as XML. +- Add or remove HTTP headers in webhooks other than those in the contact point configuration. [Using Go's templating language][using-go-templating-language] From 89148497337033a893b0c85233ef2f64c4803b4b Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Tue, 24 Oct 2023 13:02:17 +0100 Subject: [PATCH 016/180] Revert "RBAC: Allow the basic role None as option of the org role #76335 (#77033) Revert "RBAC: Allow the basic role None as option of the org role selector (#76335)" This reverts commit 43928d38af2d4129a25676e4e09551dd58935d66. --- public/app/features/admin/OrgRolePicker.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/admin/OrgRolePicker.tsx b/public/app/features/admin/OrgRolePicker.tsx index bfc0752bdd1..65f55db0f1f 100644 --- a/public/app/features/admin/OrgRolePicker.tsx +++ b/public/app/features/admin/OrgRolePicker.tsx @@ -13,7 +13,8 @@ interface Props { width?: number | 'auto'; } -const options = Object.keys(OrgRole).map((key) => ({ label: key, value: key })); +const basicRoles = Object.values(OrgRole).filter((r) => r !== OrgRole.None); +const options = basicRoles.map((r) => ({ label: r, value: r })); export function OrgRolePicker({ value, onChange, 'aria-label': ariaLabel, inputId, autoFocus, ...restProps }: Props) { return ( From e08e884607a3fa583f5cf528b01e904f371eb9e5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 14:09:05 +0200 Subject: [PATCH 017/180] Update dependency react-grid-layout to v1.4.2 (#76736) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 43 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 532dbeb01b6..87a17ef6268 100644 --- a/package.json +++ b/package.json @@ -374,7 +374,7 @@ "react-draggable": "4.4.5", "react-dropzone": "^14.2.3", "react-enable": "^3.1.0", - "react-grid-layout": "1.3.4", + "react-grid-layout": "1.4.2", "react-highlight-words": "0.20.0", "react-hook-form": "7.5.3", "react-i18next": "^12.0.0", diff --git a/yarn.lock b/yarn.lock index 8405e1e71ef..9dfb0bdfc56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15859,6 +15859,13 @@ __metadata: languageName: node linkType: hard +"fast-equals@npm:^4.0.3": + version: 4.0.3 + resolution: "fast-equals@npm:4.0.3" + checksum: 3d5935b757f9f2993e59b5164a7a9eeda0de149760495375cde14a4ed725186a7e6c1c0d58f7d42d2f91deb97f3fce1e0aad5591916ef0984278199a85c87c87 + languageName: node + linkType: hard + "fast-fifo@npm:^1.0.0": version: 1.1.0 resolution: "fast-fifo@npm:1.1.0" @@ -17378,7 +17385,7 @@ __metadata: react-draggable: 4.4.5 react-dropzone: ^14.2.3 react-enable: ^3.1.0 - react-grid-layout: 1.3.4 + react-grid-layout: 1.4.2 react-highlight-words: 0.20.0 react-hook-form: 7.5.3 react-i18next: ^12.0.0 @@ -24698,7 +24705,7 @@ __metadata: languageName: node linkType: hard -"react-draggable@npm:4.4.5, react-draggable@npm:^4.0.0, react-draggable@npm:^4.0.3": +"react-draggable@npm:4.4.5": version: 4.4.5 resolution: "react-draggable@npm:4.4.5" dependencies: @@ -24711,6 +24718,19 @@ __metadata: languageName: node linkType: hard +"react-draggable@npm:^4.0.0, react-draggable@npm:^4.0.3, react-draggable@npm:^4.4.5": + version: 4.4.6 + resolution: "react-draggable@npm:4.4.6" + dependencies: + clsx: ^1.1.1 + prop-types: ^15.8.1 + peerDependencies: + react: ">= 16.3.0" + react-dom: ">= 16.3.0" + checksum: 9b15aac59244873ac4561c5a2bead43a56e18d406e0a5f242bd4f9d151c074530c02b99387983104bf43417292f9cf8d063e554ed08d88792235e3fbc965f1b8 + languageName: node + linkType: hard + "react-dropzone@npm:14.2.3, react-dropzone@npm:^14.2.3": version: 14.2.3 resolution: "react-dropzone@npm:14.2.3" @@ -24805,6 +24825,23 @@ __metadata: languageName: node linkType: hard +"react-grid-layout@npm:1.4.2": + version: 1.4.2 + resolution: "react-grid-layout@npm:1.4.2" + dependencies: + clsx: ^2.0.0 + fast-equals: ^4.0.3 + prop-types: ^15.8.1 + react-draggable: ^4.4.5 + react-resizable: ^3.0.5 + resize-observer-polyfill: ^1.5.1 + peerDependencies: + react: ">= 16.3.0" + react-dom: ">= 16.3.0" + checksum: a052d38c290b18e1c513a3b939757c239887009b7f175814b472d007708962870960d3b20d4a15dbdeb955ac55434ce6c5e5e5f8c850cfbdcc90f75c318f1d58 + languageName: node + linkType: hard + "react-highlight-words@npm:0.20.0": version: 0.20.0 resolution: "react-highlight-words@npm:0.20.0" @@ -25072,7 +25109,7 @@ __metadata: languageName: node linkType: hard -"react-resizable@npm:3.0.5, react-resizable@npm:^3.0.4": +"react-resizable@npm:3.0.5, react-resizable@npm:^3.0.4, react-resizable@npm:^3.0.5": version: 3.0.5 resolution: "react-resizable@npm:3.0.5" dependencies: From 813a7625a5195daf1bf01ea5effbcac1cf864efd Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Tue, 24 Oct 2023 13:52:55 +0100 Subject: [PATCH 018/180] RBAC: Default to None role on SA creation if accesscontrol enabled (#77035) * Check accesscontrol enabled on service account creation * fix test * linting --- .../features/serviceaccounts/ServiceAccountCreatePage.test.tsx | 2 +- .../app/features/serviceaccounts/ServiceAccountCreatePage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/serviceaccounts/ServiceAccountCreatePage.test.tsx b/public/app/features/serviceaccounts/ServiceAccountCreatePage.test.tsx index ccb200df8f4..7d53a87ffa2 100644 --- a/public/app/features/serviceaccounts/ServiceAccountCreatePage.test.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountCreatePage.test.tsx @@ -83,7 +83,7 @@ describe('ServiceAccountCreatePage tests', () => { await waitFor(() => expect(postMock).toHaveBeenCalledWith('/api/serviceaccounts/', { name: 'Data source scavenger', - role: 'None', + role: 'Viewer', }) ); }); diff --git a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx index 70425722dca..725969145f0 100644 --- a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx @@ -29,7 +29,7 @@ export const ServiceAccountCreatePage = ({}: Props): JSX.Element => { const [serviceAccount, setServiceAccount] = useState({ id: 0, orgId: contextSrv.user.orgId, - role: OrgRole.None, + role: contextSrv.licensedAccessControlEnabled() ? OrgRole.None : OrgRole.Viewer, tokens: 0, name: '', login: '', From 765defea1ec23118b8220236eb9c78ea3d5881da Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Tue, 24 Oct 2023 15:09:30 +0200 Subject: [PATCH 019/180] Loki Queries: Query Splitting enabled by default (#75876) * Loki Query Splitting: enable by default * Query splitting: add gdev dashboard * Update testdata file * Update devenv/dev-dashboards/datasource-loki/loki_query_splitting.json Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * Revert "Update testdata file" This reverts commit 5a891ba1f2b938746908382373c490e502848193. * Update feature-toggles readme --------- Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> --- .../datasource-loki/loki_query_splitting.json | 900 ++++++++++++++++++ devenv/jsonnet/dev-dashboards.libsonnet | 7 + .../feature-toggles/index.md | 2 +- pkg/services/featuremgmt/registry.go | 3 +- pkg/services/featuremgmt/toggles_gen.csv | 2 +- 5 files changed, 911 insertions(+), 3 deletions(-) create mode 100644 devenv/dev-dashboards/datasource-loki/loki_query_splitting.json diff --git a/devenv/dev-dashboards/datasource-loki/loki_query_splitting.json b/devenv/dev-dashboards/datasource-loki/loki_query_splitting.json new file mode 100644 index 00000000000..35285857acc --- /dev/null +++ b/devenv/dev-dashboards/datasource-loki/loki_query_splitting.json @@ -0,0 +1,900 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 7004, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "description": "Transformations:\n- Count\n- Sort by\n- Limit 10", + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "editorMode": "code", + "expr": "{place=\"luna\"} | logfmt | label=\"val2\" | float > 60", + "maxLines": 5000, + "queryType": "range", + "refId": "A" + } + ], + "title": "Split logs", + "transformations": [ + { + "id": "calculateField", + "options": { + "alias": "id_count", + "mode": "reduceRow", + "reduce": { + "include": [ + "id" + ], + "reducer": "count" + }, + "replaceFields": false + } + }, + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "desc": true, + "field": "tsNs" + } + ] + } + }, + { + "id": "limit", + "options": { + "limitField": 10 + } + } + ], + "type": "logs" + }, + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "description": "Transformations:\n- Count\n- Sort by\n- Limit 10", + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "editorMode": "code", + "expr": "{place=\"luna\"} | logfmt | label=\"val2\" | float > 60", + "maxLines": 5000, + "queryType": "range", + "refId": "do-not-chunk" + } + ], + "title": "Logs without splitting", + "transformations": [ + { + "id": "calculateField", + "options": { + "alias": "id_count", + "mode": "reduceRow", + "reduce": { + "include": [ + "id" + ], + "reducer": "count" + }, + "replaceFields": false + } + }, + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "desc": true, + "field": "tsNs" + } + ] + } + }, + { + "id": "limit", + "options": {} + } + ], + "type": "logs" + }, + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 7 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "editorMode": "code", + "expr": "count_over_time({place=\"luna\"} | logfmt | label=\"val2\" | float > 60 | drop wave, _entry, level, float, counter [$__auto])", + "queryType": "range", + "refId": "A" + } + ], + "title": "Split TS", + "transformations": [], + "type": "timeseries" + }, + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 7 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "editorMode": "code", + "expr": "count_over_time({place=\"luna\"} | logfmt | label=\"val2\" | float > 60 | drop wave, _entry, level, float, counter [$__auto])", + "queryType": "range", + "refId": "do-not-chunk" + } + ], + "title": "TS without splitting", + "transformations": [], + "type": "timeseries" + }, + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 0, + "y": 15 + }, + "id": 5, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "editorMode": "code", + "expr": "{place=\"luna\"} | logfmt", + "maxLines": 5000, + "queryType": "range", + "refId": "A" + } + ], + "title": "Logs with filter transformation", + "transformations": [ + { + "id": "filterByValue", + "options": { + "filters": [ + { + "config": { + "id": "isNull", + "options": {} + }, + "fieldName": "TraceID" + } + ], + "match": "any", + "type": "exclude" + } + } + ], + "type": "logs" + }, + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 12, + "y": 15 + }, + "id": 6, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "editorMode": "code", + "expr": "{place=\"luna\"} | logfmt", + "maxLines": 5000, + "queryType": "range", + "refId": "do-not-chunk" + } + ], + "title": "Logs with filter transformation, no splitting", + "transformations": [ + { + "id": "filterByValue", + "options": { + "filters": [ + { + "config": { + "id": "isNull", + "options": {} + }, + "fieldName": "TraceID" + } + ], + "match": "any", + "type": "exclude" + } + } + ], + "type": "logs" + }, + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 20 + }, + "id": 7, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "10.2.0-61469", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "editorMode": "code", + "expr": "{place=\"luna\", age=\"new\"}", + "maxLines": 5000, + "queryType": "range", + "refId": "A" + } + ], + "title": "Logs with extract fields transformation", + "transformations": [ + { + "id": "extractFields", + "options": { + "format": "json", + "keepTime": true, + "replace": true, + "source": "labels" + } + }, + { + "id": "calculateField", + "options": { + "alias": "Row", + "mode": "index", + "reduce": { + "reducer": "sum" + }, + "replaceFields": false + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 20 + }, + "id": 8, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "10.2.0-61469", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "editorMode": "code", + "expr": "{place=\"luna\", age=\"new\"}", + "maxLines": 5000, + "queryType": "range", + "refId": "do-not-chunk" + } + ], + "title": "Logs with extract fields transformation, no splitting", + "transformations": [ + { + "id": "extractFields", + "options": { + "format": "json", + "keepTime": true, + "replace": true, + "source": "labels" + } + }, + { + "id": "calculateField", + "options": { + "alias": "Row", + "mode": "index", + "reduce": { + "reducer": "sum" + }, + "replaceFields": false + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 28 + }, + "id": 9, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "10.2.0-61469", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "editorMode": "code", + "expr": "{place=\"moon\"}", + "maxLines": 5, + "queryType": "range", + "refId": "A" + } + ], + "title": "Logs with extract key=value and organize", + "transformations": [ + { + "id": "extractFields", + "options": { + "format": "auto", + "keepTime": true, + "replace": true, + "source": "Line" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "31": true, + "32": true, + "33": true, + "34": true, + "35": true, + "36": true, + "37": true, + "38": true, + "39": true, + "40": true, + "41": true, + "42": true, + "43": true, + "44": true, + "45": true, + "46": true, + "2023-09-20T14": true, + "33+00": true, + "caller": true, + "main.go": true, + "t": true, + "ts": true + }, + "indexByName": {}, + "renameByName": { + "level": "nivel", + "ts": "" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 28 + }, + "id": 10, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "10.2.0-61469", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "gdev-loki" + }, + "editorMode": "code", + "expr": "{place=\"moon\"}", + "maxLines": 5, + "queryType": "range", + "refId": "do-not-chunk" + } + ], + "title": "Logs with extract key=value and organize, no splitting", + "transformations": [ + { + "id": "extractFields", + "options": { + "format": "auto", + "keepTime": true, + "replace": true, + "source": "Line" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "31": true, + "32": true, + "33": true, + "34": true, + "35": true, + "36": true, + "37": true, + "38": true, + "39": true, + "40": true, + "41": true, + "42": true, + "43": true, + "44": true, + "45": true, + "46": true, + "2023-09-20T14": true, + "33+00": true, + "caller": true, + "main.go": true, + "t": true, + "ts": true + }, + "indexByName": {}, + "renameByName": { + "level": "nivel", + "ts": "" + } + } + } + ], + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 38, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Datasource tests - Loki query splitting", + "uid": "dc4ec947-7e6b-4c3b-be8f-0abec7b0652a", + "version": 13, + "weekStart": "" + } \ No newline at end of file diff --git a/devenv/jsonnet/dev-dashboards.libsonnet b/devenv/jsonnet/dev-dashboards.libsonnet index 73541ca8bb1..d88cdfd2595 100644 --- a/devenv/jsonnet/dev-dashboards.libsonnet +++ b/devenv/jsonnet/dev-dashboards.libsonnet @@ -387,6 +387,13 @@ local dashboard = grafana.dashboard; id: 0, } }, + dashboard.new('loki_query_splitting', import '../dev-dashboards/datasource-loki/loki_query_splitting.json') + + resource.addMetadata('folder', 'dev-dashboards') + + { + spec+: { + id: 0, + } + }, dashboard.new('migrations', import '../dev-dashboards/migrations/migrations.json') + resource.addMetadata('folder', 'dev-dashboards') + { 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 b3fd94abc29..295f4d8e7cf 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -34,6 +34,7 @@ Some features are enabled by default. You can disable these feature by setting t | `emptyDashboardPage` | Enable the redesigned user interface of a dashboard page that includes no panels | Yes | | `disablePrometheusExemplarSampling` | Disable Prometheus exemplar sampling | | | `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes | +| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | Yes | | `gcomOnlyExternalOrgRoleSync` | Prohibits a user from changing organization roles synced with Grafana Cloud auth provider | | | `prometheusMetricEncyclopedia` | Adds the metrics explorer component to the Prometheus query builder as an option in metric select | Yes | | `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | Yes | @@ -104,7 +105,6 @@ Experimental features might be changed or removed without prior notice. | `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | | `alertingBacktesting` | Rule backtesting API for alerting | | `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | -| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | | `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries | | `individualCookiePreferences` | Support overriding cookie preferences per user | | `clientTokenRotation` | Replaces the current in-request token rotation so that the client initiates the rotation | diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index a85f8937398..70fd828c1c8 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -301,9 +301,10 @@ var ( { Name: "lokiQuerySplitting", Description: "Split large interval queries into subqueries with smaller time intervals", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, FrontendOnly: true, Owner: grafanaObservabilityLogsSquad, + Expression: "true", // turned on by default }, { Name: "lokiQuerySplittingConfig", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 4cd76061c5c..570056669d2 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -42,7 +42,7 @@ alertingBacktesting,experimental,@grafana/alerting-squad,false,false,false,false editPanelCSVDragAndDrop,experimental,@grafana/grafana-bi-squad,false,false,false,true alertingNoNormalState,preview,@grafana/alerting-squad,false,false,false,false logsContextDatasourceUi,GA,@grafana/observability-logs,false,false,false,true -lokiQuerySplitting,experimental,@grafana/observability-logs,false,false,false,true +lokiQuerySplitting,GA,@grafana/observability-logs,false,false,false,true lokiQuerySplittingConfig,experimental,@grafana/observability-logs,false,false,false,true individualCookiePreferences,experimental,@grafana/backend-platform,false,false,false,false gcomOnlyExternalOrgRoleSync,GA,@grafana/grafana-authnz-team,false,false,false,false From b8d005ae236302f485cff0dc9fd4eaacf0058fbc Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:16:10 +0200 Subject: [PATCH 020/180] Plugins: Improvements to NodeGraph (#76879) --- .betterer.results | 3 +- .../visualizations/node-graph/index.md | 3 + packages/grafana-data/src/utils/nodeGraph.ts | 7 ++ .../nodeGraphUtils.ts | 12 ++- public/app/plugins/panel/nodeGraph/Edge.tsx | 78 +++++++++++-------- .../panel/nodeGraph/EdgeArrowMarker.tsx | 22 ++++-- .../app/plugins/panel/nodeGraph/Node.test.tsx | 1 + public/app/plugins/panel/nodeGraph/Node.tsx | 21 +++-- .../app/plugins/panel/nodeGraph/NodeGraph.tsx | 2 - .../plugins/panel/nodeGraph/layout.test.ts | 3 + public/app/plugins/panel/nodeGraph/types.ts | 3 + .../app/plugins/panel/nodeGraph/utils.test.ts | 4 + public/app/plugins/panel/nodeGraph/utils.ts | 31 +++++++- 13 files changed, 138 insertions(+), 52 deletions(-) diff --git a/.betterer.results b/.betterer.results index b5af32e93b2..e3f389eba41 100644 --- a/.betterer.results +++ b/.betterer.results @@ -7367,7 +7367,8 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "4"], [0, 0, 0, "Styles should be written using objects.", "5"], [0, 0, 0, "Styles should be written using objects.", "6"], - [0, 0, 0, "Styles should be written using objects.", "7"] + [0, 0, 0, "Styles should be written using objects.", "7"], + [0, 0, 0, "Styles should be written using objects.", "8"] ], "public/app/plugins/panel/nodeGraph/NodeGraph.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], diff --git a/docs/sources/panels-visualizations/visualizations/node-graph/index.md b/docs/sources/panels-visualizations/visualizations/node-graph/index.md index d4ac4fc5d4a..7e7a3d2c588 100644 --- a/docs/sources/panels-visualizations/visualizations/node-graph/index.md +++ b/docs/sources/panels-visualizations/visualizations/node-graph/index.md @@ -108,6 +108,8 @@ Optional fields: | mainstat | string/number | First stat shown in the overlay when hovering over the edge. It can be a string showing the value as is or it can be a number. If it is a number, any unit associated with that field is also shown | | secondarystat | string/number | Same as mainStat, but shown right under it. | | detail\_\_\* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the edge. Use `config.displayName` for more human readable label. | +| thickness | number | The thickness of the edge. Default: `1` | +| highlighted | boolean | Sets whether the edge should be highlighted. Useful, for example, to represent a specific path in the graph by highlighting several nodes and edges. Default: `false` | ### Nodes data frame structure @@ -130,3 +132,4 @@ Optional fields: | color | string/number | Can be used to specify a single color instead of using the `arc__` fields to specify color sections. It can be either a string which should then be an acceptable HTML color string or it can be a number in which case the behaviour depends on `field.config.color.mode` setting. This can be for example used to create gradient colors controlled by the field value. | | icon | string | Name of the icon to show inside the node instead of the default stats. Only Grafana built in icons are allowed (see the available icons [here](https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview)). | | nodeRadius | number | Radius value in pixels. Used to manage node size. | +| highlighted | boolean | Sets whether the node should be highlighted. Useful for example to represent a specific path in the graph by highlighting several nodes and edges. Default: `false` | diff --git a/packages/grafana-data/src/utils/nodeGraph.ts b/packages/grafana-data/src/utils/nodeGraph.ts index ce063f6fa43..b846348a7a7 100644 --- a/packages/grafana-data/src/utils/nodeGraph.ts +++ b/packages/grafana-data/src/utils/nodeGraph.ts @@ -26,5 +26,12 @@ export enum NodeGraphDataFrameFieldNames { // Prefix for fields which will be shown in a context menu [nodes + edges] detail = 'detail__', + // Radius of the node [nodes] nodeRadius = 'noderadius', + + // Thickness of the edge [edges] + thickness = 'thickness', + + // Whether the node or edge should be highlighted (e.g., shown in red) in the UI + highlighted = 'highlighted', } diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/nodeGraphUtils.ts b/public/app/plugins/datasource/grafana-testdata-datasource/nodeGraphUtils.ts index cd74289ba7f..3bf1af6280b 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/nodeGraphUtils.ts +++ b/public/app/plugins/datasource/grafana-testdata-datasource/nodeGraphUtils.ts @@ -105,6 +105,10 @@ export function generateRandomNodes(count = 10) { values: [], type: FieldType.number, }, + [NodeGraphDataFrameFieldNames.highlighted]: { + values: [], + type: FieldType.boolean, + }, }; const nodeFrame = new MutableDataFrame({ @@ -123,6 +127,8 @@ export function generateRandomNodes(count = 10) { { name: NodeGraphDataFrameFieldNames.source, values: [], type: FieldType.string, config: {} }, { name: NodeGraphDataFrameFieldNames.target, values: [], type: FieldType.string, config: {} }, { name: NodeGraphDataFrameFieldNames.mainStat, values: [], type: FieldType.number, config: {} }, + { name: NodeGraphDataFrameFieldNames.highlighted, values: [], type: FieldType.boolean, config: {} }, + { name: NodeGraphDataFrameFieldNames.thickness, values: [], type: FieldType.number, config: {} }, ], meta: { preferredVisualisationType: 'nodeGraph' }, length: 0, @@ -139,7 +145,8 @@ export function generateRandomNodes(count = 10) { nodeFields.arc__errors.values.push(node.error); const rnd = Math.random(); nodeFields[NodeGraphDataFrameFieldNames.icon].values.push(rnd > 0.9 ? 'database' : rnd < 0.1 ? 'cloud' : ''); - nodeFields[NodeGraphDataFrameFieldNames.nodeRadius].values.push(rnd > 0.5 ? 30 : 40); + nodeFields[NodeGraphDataFrameFieldNames.nodeRadius].values.push(Math.max(rnd * 100, 30)); // ensure a minimum radius of 30 or icons will not fit well in the node + nodeFields[NodeGraphDataFrameFieldNames.highlighted].values.push(Math.random() > 0.5); for (const edge of node.edges) { const id = `${node.id}--${edge}`; @@ -152,6 +159,8 @@ export function generateRandomNodes(count = 10) { edgesFrame.fields[1].values.push(node.id); edgesFrame.fields[2].values.push(edge); edgesFrame.fields[3].values.push(Math.random() * 100); + edgesFrame.fields[4].values.push(Math.random() > 0.5); + edgesFrame.fields[5].values.push(Math.ceil(Math.random() * 15)); } } edgesFrame.length = edgesFrame.fields[0].values.length; @@ -171,6 +180,7 @@ function makeRandomNode(index: number) { stat1: Math.random(), stat2: Math.random(), edges: [], + highlighted: Math.random() > 0.5, }; } diff --git a/public/app/plugins/panel/nodeGraph/Edge.tsx b/public/app/plugins/panel/nodeGraph/Edge.tsx index ae2e7572b1e..b31c49be490 100644 --- a/public/app/plugins/panel/nodeGraph/Edge.tsx +++ b/public/app/plugins/panel/nodeGraph/Edge.tsx @@ -1,9 +1,13 @@ import React, { MouseEvent, memo } from 'react'; -import { nodeR } from './Node'; +import { EdgeArrowMarker } from './EdgeArrowMarker'; +import { computeNodeCircumferenceStrokeWidth, nodeR } from './Node'; import { EdgeDatum, NodeDatum } from './types'; import { shortenLine } from './utils'; +export const highlightedEdgeColor = '#a00'; +export const defaultEdgeColor = '#999'; + interface Props { edge: EdgeDatum; hovering: boolean; @@ -11,6 +15,7 @@ interface Props { onMouseEnter: (id: string) => void; onMouseLeave: (id: string) => void; } + export const Edge = memo(function Edge(props: Props) { const { edge, onClick, onMouseEnter, onMouseLeave, hovering } = props; @@ -21,6 +26,7 @@ export const Edge = memo(function Edge(props: Props) { sourceNodeRadius: number; targetNodeRadius: number; }; + const arrowHeadHeight = 10 + edge.thickness * 2; // resized value, just to make the UI nicer // As the nodes have some radius we want edges to end outside of the node circle. const line = shortenLine( @@ -30,39 +36,47 @@ export const Edge = memo(function Edge(props: Props) { x2: target.x!, y2: target.y!, }, - sourceNodeRadius || nodeR, - targetNodeRadius || nodeR + sourceNodeRadius + computeNodeCircumferenceStrokeWidth(sourceNodeRadius) / 2 || nodeR, + targetNodeRadius + computeNodeCircumferenceStrokeWidth(targetNodeRadius) / 2 || nodeR, + arrowHeadHeight ); + const markerId = `triangle-${edge.id}`; + const coloredMarkerId = `triangle-colored-${edge.id}`; + return ( - onClick(event, edge)} - style={{ cursor: 'pointer' }} - aria-label={`Edge from: ${source.id} to: ${target.id}`} - > - - { - onMouseEnter(edge.id); - }} - onMouseLeave={() => { - onMouseLeave(edge.id); - }} - /> - + <> + + + onClick(event, edge)} + style={{ cursor: 'pointer' }} + aria-label={`Edge from: ${source.id} to: ${target.id}`} + > + + { + onMouseEnter(edge.id); + }} + onMouseLeave={() => { + onMouseLeave(edge.id); + }} + /> + + ); }); diff --git a/public/app/plugins/panel/nodeGraph/EdgeArrowMarker.tsx b/public/app/plugins/panel/nodeGraph/EdgeArrowMarker.tsx index a56a97c85d3..ac53e42a78b 100644 --- a/public/app/plugins/panel/nodeGraph/EdgeArrowMarker.tsx +++ b/public/app/plugins/panel/nodeGraph/EdgeArrowMarker.tsx @@ -1,23 +1,33 @@ import React from 'react'; +import { defaultEdgeColor } from './Edge'; + /** * In SVG you need to supply this kind of marker that can be then referenced from a line segment as an ending of the * line turning in into arrow. Needs to be included in the svg element and then referenced as markerEnd="url(#triangle)" */ -export function EdgeArrowMarker() { +export function EdgeArrowMarker({ + id = 'triangle', + fill = defaultEdgeColor, + headHeight = 10, +}: { + id?: string; + fill?: string; + headHeight?: number; +}) { return ( - + ); diff --git a/public/app/plugins/panel/nodeGraph/Node.test.tsx b/public/app/plugins/panel/nodeGraph/Node.test.tsx index a831e673b6b..c6fe4e91750 100644 --- a/public/app/plugins/panel/nodeGraph/Node.test.tsx +++ b/public/app/plugins/panel/nodeGraph/Node.test.tsx @@ -69,4 +69,5 @@ const nodeDatum = { mainStat: { name: 'stat', values: [1234], type: FieldType.number, config: {} }, secondaryStat: { name: 'stat2', values: [9876], type: FieldType.number, config: {} }, arcSections: [], + highlighted: false, }; diff --git a/public/app/plugins/panel/nodeGraph/Node.tsx b/public/app/plugins/panel/nodeGraph/Node.tsx index f111a8de786..f87da31b7ac 100644 --- a/public/app/plugins/panel/nodeGraph/Node.tsx +++ b/public/app/plugins/panel/nodeGraph/Node.tsx @@ -11,6 +11,7 @@ import { NodeDatum } from './types'; import { statToString } from './utils'; export const nodeR = 40; +export const highlightedNodeColor = '#a00'; const getStyles = (theme: GrafanaTheme2, hovering: HoverState) => ({ mainGroup: css` @@ -24,6 +25,10 @@ const getStyles = (theme: GrafanaTheme2, hovering: HoverState) => ({ fill: ${theme.components.panel.background}; `, + filledCircle: css` + fill: ${highlightedNodeColor}; + `, + hoverCircle: css` opacity: 0.5; fill: transparent; @@ -66,6 +71,8 @@ const getStyles = (theme: GrafanaTheme2, hovering: HoverState) => ({ `, }); +export const computeNodeCircumferenceStrokeWidth = (nodeRadius: number) => Math.ceil(nodeRadius * 0.075); + export const Node = memo(function Node(props: { node: NodeDatum; hovering: HoverState; @@ -78,6 +85,7 @@ export const Node = memo(function Node(props: { const styles = getStyles(theme, hovering); const isHovered = hovering === 'active'; const nodeRadius = node.nodeRadius?.values[node.dataFrameRowIndex] || nodeR; + const strokeWidth = computeNodeCircumferenceStrokeWidth(nodeRadius); if (!(node.x !== undefined && node.y !== undefined)) { return null; @@ -87,13 +95,13 @@ export const Node = memo(function Node(props: { {isHovered && ( - + )} @@ -172,14 +180,15 @@ function ColorCircle(props: { node: NodeDatum }) { const fullStat = node.arcSections.find((s) => s.values[node.dataFrameRowIndex] >= 1); const theme = useTheme2(); const nodeRadius = node.nodeRadius?.values[node.dataFrameRowIndex] || nodeR; + const strokeWidth = computeNodeCircumferenceStrokeWidth(nodeRadius); if (fullStat) { - // Doing arc with path does not work well so it's better to just do a circle in that case + // Drawing a full circle with a `path` tag does not work well, it's better to use a `circle` tag in that case return ( ); acc.elements.push(el); diff --git a/public/app/plugins/panel/nodeGraph/NodeGraph.tsx b/public/app/plugins/panel/nodeGraph/NodeGraph.tsx index b029c242af3..ccd14fd12b3 100644 --- a/public/app/plugins/panel/nodeGraph/NodeGraph.tsx +++ b/public/app/plugins/panel/nodeGraph/NodeGraph.tsx @@ -7,7 +7,6 @@ import { DataFrame, GrafanaTheme2, LinkModel } from '@grafana/data'; import { Icon, Spinner, useStyles2 } from '@grafana/ui'; import { Edge } from './Edge'; -import { EdgeArrowMarker } from './EdgeArrowMarker'; import { EdgeLabel } from './EdgeLabel'; import { Legend } from './Legend'; import { Marker } from './Marker'; @@ -208,7 +207,6 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit }: Props) { className={styles.mainGroup} style={{ transform: `scale(${scale}) translate(${Math.floor(position.x)}px, ${Math.floor(position.y)}px)` }} > - {!config.gridLayout && ( = {}) { ], color: colorField, dataFrameRowIndex: 0, + highlighted: false, id: '0', incoming: 0, mainStat: { @@ -334,6 +335,8 @@ function makeEdgeDatum(id: string, index: number, mainStat = '', secondaryStat = target: id.split('--')[1], sourceNodeRadius: 40, targetNodeRadius: 40, + highlighted: false, + thickness: 1, }; } @@ -346,5 +349,6 @@ function makeNodeFromEdgeDatum(options: Partial = {}): NodeDatum { subTitle: '', title: 'service:0', ...options, + highlighted: false, }; } diff --git a/public/app/plugins/panel/nodeGraph/utils.ts b/public/app/plugins/panel/nodeGraph/utils.ts index 695ecdf523c..97fb11e3061 100644 --- a/public/app/plugins/panel/nodeGraph/utils.ts +++ b/public/app/plugins/panel/nodeGraph/utils.ts @@ -15,19 +15,32 @@ import { EdgeDatum, GraphFrame, NodeDatum, NodeDatumFromEdge, NodeGraphOptions } type Line = { x1: number; y1: number; x2: number; y2: number }; /** - * Makes line shorter while keeping the middle in he same place. + * Makes line shorter while keeping its middle in the same place. + * This is manly used to add some empty space between an edge line and its source and target nodes, to make it nicer. + * + * @param line a line, where x1 and y1 are the coordinates of the source node center, and x2 and y2 are the coordinates of the target node center + * @param sourceNodeRadius radius of the source node (possibly taking into account the thickness of the node circumference line, etc.) + * @param targetNodeRadius radius of the target node (possibly taking into account the thickness of the node circumference line, etc.) + * @param arrowHeadHeight height of the arrow head (in pixels) */ -export function shortenLine(line: Line, sourceNodeRadius: number, targetNodeRadius: number): Line { +export function shortenLine(line: Line, sourceNodeRadius: number, targetNodeRadius: number, arrowHeadHeight = 1): Line { const vx = line.x2 - line.x1; const vy = line.y2 - line.y1; const mag = Math.sqrt(vx * vx + vy * vy); const cosine = (line.x2 - line.x1) / mag; const sine = (line.y2 - line.y1) / mag; + const scaledThickness = arrowHeadHeight - arrowHeadHeight / 10; + + // Reduce the line length (along its main direction) by: + // - the radius of the source node + // - the radius of the target node, + // - a constant value, just to add some empty space + // - the height of the arrow head; the bigger the arrow head, the better is to add even more empty space return { x1: line.x1 + cosine * (sourceNodeRadius + 5), y1: line.y1 + sine * (sourceNodeRadius + 5), - x2: line.x2 - cosine * (targetNodeRadius + 5), - y2: line.y2 - sine * (targetNodeRadius + 5), + x2: line.x2 - cosine * (targetNodeRadius + 3 + scaledThickness), + y2: line.y2 - sine * (targetNodeRadius + 3 + scaledThickness), }; } @@ -42,6 +55,7 @@ export type NodeFields = { color?: Field; icon?: Field; nodeRadius?: Field; + highlighted?: Field; }; export function getNodeFields(nodes: DataFrame): NodeFields { @@ -61,6 +75,7 @@ export function getNodeFields(nodes: DataFrame): NodeFields { color: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.color), icon: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.icon), nodeRadius: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.nodeRadius.toLowerCase()), + highlighted: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.highlighted.toLowerCase()), }; } @@ -71,6 +86,8 @@ export type EdgeFields = { mainStat?: Field; secondaryStat?: Field; details: Field[]; + highlighted?: Field; + thickness?: Field; }; export function getEdgeFields(edges: DataFrame): EdgeFields { @@ -86,6 +103,8 @@ export function getEdgeFields(edges: DataFrame): EdgeFields { mainStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.mainStat.toLowerCase()), secondaryStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.secondaryStat.toLowerCase()), details: findFieldsByPrefix(edges, NodeGraphDataFrameFieldNames.detail.toLowerCase()), + highlighted: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.highlighted.toLowerCase()), + thickness: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.thickness.toLowerCase()), }; } @@ -215,6 +234,8 @@ function processEdges(edges: DataFrame, edgeFields: EdgeFields, nodesMap: { [id: secondaryStat: edgeFields.secondaryStat ? statToString(edgeFields.secondaryStat.config, edgeFields.secondaryStat.values[index]) : '', + highlighted: edgeFields.highlighted?.values[index] || false, + thickness: edgeFields.thickness?.values[index] || 1, }; }); } @@ -286,6 +307,7 @@ function makeSimpleNodeDatum(name: string, index: number): NodeDatumFromEdge { dataFrameRowIndex: index, incoming: 0, arcSections: [], + highlighted: false, }; } @@ -302,6 +324,7 @@ function makeNodeDatum(id: string, nodeFields: NodeFields, index: number): NodeD color: nodeFields.color, icon: nodeFields.icon?.values[index] || '', nodeRadius: nodeFields.nodeRadius, + highlighted: nodeFields.highlighted?.values[index] || false, }; } From b2eda16023249cd47bee796e269c449271fc3e27 Mon Sep 17 00:00:00 2001 From: lwandz13 <126723338+lwandz13@users.noreply.github.com> Date: Tue, 24 Oct 2023 08:42:40 -0500 Subject: [PATCH 021/180] Docs: Updates based on UI changes, support request (#76907) * added additional links on Lucene queries * cosmetic updates to query editor * updated config doc to reflect UI changes * removed Explore elements from query editor doc * ran prettier --- .../configure-elasticsearch-data-source.md | 70 ++++++++++++------- .../elasticsearch/query-editor/index.md | 19 ++--- 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/docs/sources/datasources/elasticsearch/configure-elasticsearch-data-source.md b/docs/sources/datasources/elasticsearch/configure-elasticsearch-data-source.md index 8bb5a6a6daa..de5e927d667 100644 --- a/docs/sources/datasources/elasticsearch/configure-elasticsearch-data-source.md +++ b/docs/sources/datasources/elasticsearch/configure-elasticsearch-data-source.md @@ -21,23 +21,22 @@ weight: 200 # Configure the Elasticsearch data source Grafana ships with built-in support for Elasticsearch. -You can make many types of queries to visualize logs or metrics stored in Elasticsearch, and annotate graphs with log events stored in Elasticsearch. - -For general documentation on querying data sources in Grafana, see [Query and transform data][]. +You can create a variety of queries to visualize logs or metrics stored in Elasticsearch, and annotate graphs with log events stored in Elasticsearch. For instructions on how to add a data source to Grafana, refer to the [administration documentation][]. -Only users with the organization administrator role can add data sources. + +Only users with the organization `administrator` role can add data sources. Administrators can also [configure the data source via YAML](#provision-the-data-source) with Grafana's provisioning system. -## Configure the data source +## Add the data source To add the Elasticsearch data source, complete the following steps: 1. Click **Connections** in the left-side menu. 1. Under **Connections**, click **Add new connection**. 1. Enter `Elasticsearch` in the search bar. -1. Select **Elasticsearch data source**. -1. Click **Create a Elasticsearch data source** in the upper right. +1. Click **Elasticsearch** under the **Data source** section. +1. Click **Add new data source** in the upper right. You will be taken to the **Settings** tab where you will set up your Elasticsearch configuration. @@ -51,40 +50,55 @@ The first option to configure is the name of your connection: - **Default** - Toggle to select as the default data source option. When you go to a dashboard panel or Explore, this will be the default selected data source. -### HTTP section +## Connection -- **URL** - The URL of your Elasticsearch server. If your Elasticsearch server is local, use ``. If it is on a server within a network, this is the URL with port where you are running Elasticsearch. Example: ``. +Connect the Elasticsearch data source by specifying a URL. -- **Allowed cookies** - Specify cookies by name that should be forwarded to the data source. The Grafana proxy deletes all forwarded cookies by default. +- **URL** - The URL of your Elasticsearch server. If your Elasticsearch server is local, use `http://localhost:9200`. If it is on a server within a network, this is the URL with the port where you are running Elasticsearch. Example: `http://elasticsearch.example.orgname:9200`. -- **Timeout** - The HTTP request timeout. This must be in seconds. There is no default, so this setting is up to you. - -### Auth section +## Authentication There are several authentication methods you can choose in the Authentication section. +Select one of the following authentication methods from the dropdown menu. + +- **Basic authentication** - The most common authentication method. Use your `data source` user name and `data source` password to connect. + +- **Forward OAuth identity** - Forward the OAuth access token (and the OIDC ID token if available) of the user querying the data source. + +- **No authentication** - Make the data source available without authentication. Grafana recommends using some type of authentication method. + + + +### TLS settings {{% admonition type="note" %}} Use TLS (Transport Layer Security) for an additional layer of security when working with Elasticsearch. For information on setting up TLS encryption with Elasticsearch see [Configure TLS](https://www.elastic.co/guide/en/elasticsearch/reference/8.8/configuring-tls.html#configuring-tls). You must add TLS settings to your Elasticsearch configuration file **prior** to setting these options in Grafana. {{% /admonition %}} -- **Basic authentication** - The most common authentication method. Use your `data source` user name and `data source` password to connect. +- **Add self-signed certificate** - Check the box to authenticate with a CA certificate. Follow the instructions of the CA (Certificate Authority) to download the certificate file. Required for verifying self-signed TLS certificates. -- **With credentials** - Toggle to enable credentials such as cookies or auth headers to be sent with cross-site requests. +- **TLS client authentication** - Check the box to authenticate with the TLS client, where the server authenticates the client. Add the `Server name`, `Client certificate` and `Client key`. The **ServerName** is used to verify the hostname on the returned certificate. The **Client certificate** can be generated from a Certificate Authority (CA) or be self-signed. The **Client key** can also be generated from a Certificate Authority (CA) or be self-signed. The client key encrypts the data between client and server. -- **TLS client authentication** - Toggle to use client authentication. When enabled, add the `Server name`, `Client cert` and `Client key`. The client provides a certificate that is validated by the server to establish the client's trusted identity. The client key encrypts the data between client and server. +- **Skip TLS certificate validation** - Check the box to bypass TLS certificate validation. Skipping TLS certificate validation is not recommended unless absolutely necessary or for testing purposes. -- **With CA cert** - Toggle to authenticate with a CA certificate. Follow the instructions of the CA (Certificate Authority) to download the certificate file. +### HTTP headers -- **Skip TLS verify** - Toggle on to bypass TLS certificate validation. - -- **Forward OAuth identity** - Forward the OAuth access token (and the OIDC ID token if available) of the user querying the data source. - -### Custom HTTP headers +Click **+ Add header** to add one or more HTTP headers. HTTP headers pass additional context and metadata about the request/response. - **Header** - Add a custom header. This allows custom headers to be passed based on the needs of your Elasticsearch instance. - **Value** - The value of the header. +## Additional settings + +Additional settings are optional settings that can be configured for more control over your data source. + +### Advanced HTTP settings + +- **Allowed cookies** - Specify cookies by name that should be forwarded to the data source. The Grafana proxy deletes all forwarded cookies by default. + +- **Timeout** - The HTTP request timeout. This must be in seconds. There is no default, so this setting is up to you. + ### Elasticsearch details The following settings are specific to the Elasticsearch data source. @@ -124,7 +138,7 @@ You can also override this setting in a dashboard panel under its data source op - **X-Pack enabled** - Toggle to enable `X-Pack`-specific features and options, which provide the [query editor]({{< relref "./query-editor" >}}) with additional aggregations, such as `Rate` and `Top Metrics`. -- **Include frozen indices** - Toggle on when the `X-Pack enabled` setting is active. You can configure Grafana to include [frozen indices](https://www.elastic.co/guide/en/elasticsearch/reference/7.13/frozen-indices.html) when performing search requests. +- **Include frozen indices** - Toggle on when the `X-Pack enabled` setting is active. Includes frozen indices in searches. You can configure Grafana to include [frozen indices](https://www.elastic.co/guide/en/elasticsearch/reference/7.13/frozen-indices.html) when performing search requests. {{% admonition type="note" %}} Frozen indices are [deprecated in Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/frozen-indices.html) since v7.14. @@ -140,7 +154,7 @@ In this section you can configure which fields the data source uses for log mess ### Data links -Data links create a link from a specified field that can be accessed in Explore's logs view. You can add multiple data links +Data links create a link from a specified field that can be accessed in Explore's logs view. You can add multiple data links by clicking **+ Add**. Each data link configuration consists of: @@ -152,6 +166,14 @@ Each data link configuration consists of: - **Internal link** - Toggle on to set an internal link. For an internal link, you can select the target data source with a data source selector. This supports only tracing data sources. +## Private data source connect (PDC) and Elasticsearch + +Use private data source connect (PDC) to connect to and query data within a secure network without opening that network to inbound traffic from Grafana Cloud. See [Private data source connect](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) for more information on how PDC works and [Configure Grafana private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/configure-pdc/#configure-grafana-private-data-source-connect-pdc) for steps on setting up a PDC connection. + +- **Private data source connect** - Click in the box to set the default PDC connection from the dropdown menu or create a new connection. + +Once you have configured your Elasticsearch data source options, click **Save & test** at the bottom to test out your data source connection. You can also remove a connection by clicking **Delete**. + {{% docs/reference %}} [administration documentation]: "/docs/grafana/ -> /docs/grafana//administration/data-source-management" [administration documentation]: "/docs/grafana-cloud/ -> /docs/grafana//administration/data-source-management" diff --git a/docs/sources/datasources/elasticsearch/query-editor/index.md b/docs/sources/datasources/elasticsearch/query-editor/index.md index f6e3cd9b1ca..8d15a47e36d 100644 --- a/docs/sources/datasources/elasticsearch/query-editor/index.md +++ b/docs/sources/datasources/elasticsearch/query-editor/index.md @@ -23,7 +23,8 @@ weight: 300 # Elasticsearch query editor -Grafana provides a query editor for Elasticsearch. Elasticsearch queries are in Lucene format. See [Query string syntax](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/query-dsl-query-string-query.html#query-string-syntax) if you are new to working with Elasticsearch. +Grafana provides a query editor for Elasticsearch. Elasticsearch queries are in Lucene format. +See [Lucene query syntax](https://www.elastic.co/guide/en/kibana/current/lucene-query.html) and and [Query string syntax](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/query-dsl-query-string-query.html#query-string-syntax) if you are new to working with Lucene queries in Elasticsearch. {{< figure src="/static/img/docs/elasticsearch/elastic-query-editor-10.1.png" max-width="800px" class="docs-image--no-shadow" caption="Elasticsearch query editor" >}} @@ -39,18 +40,6 @@ Elasticsearch groups aggregations into three categories: - **Pipeline** - Elasticsearch pipeline aggregations work with inputs or metrics created from other aggregations (not documents or fields). There are parent and sibling and sibling pipeline aggregations. See [Pipeline aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/search-aggregations-pipeline.html) for additional information. -## Common options - -There are several different types of queries you can create using the Elasticsearch query editor. The following options are available for all query types. - -### Add query - -Regardless of query type, you can create multiple queries by clicking **+ Add query**. - -### Query inspector - -Click **Query inspector** to get detailed statistics regarding your query. Query inspector functions as a kind of debugging tool that "inspects" your query. It provides query statistics under **Stats**, request response time under **Query**, data frame details under **{} JSON**, and the shape of your data under **Data**. - ## Select a query type There are three types of queries you can create with the Elasticsearch query builder. Each type is explained in detail below. @@ -70,13 +59,13 @@ Metrics queries aggregate data and produce a variety of calculations such as cou - min - see [Min aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/search-aggregations-metrics-min-aggregation.html) - extended stats - see [Extended stats aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-extendedstats-aggregation.html) - percentiles - see [Percentiles aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/search-aggregations-metrics-percentile-aggregation.html) - - unique count - see [Cardinlaity aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/search-aggregations-metrics-cardinality-aggregation.html) + - unique count - see [Cardinality aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/search-aggregations-metrics-cardinality-aggregation.html) - top metrics - see [Top metrics aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/search-aggregations-metrics-top-metrics.html) - rate - see [Rate aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/search-aggregations-metrics-rate-aggregation.html) You can select multiple metrics and group by multiple terms or filters when using the Elasticsearch query editor. -Use the **plus icon** to the right to add multiple metrics to your query. Click on the **eye icon** next to "Metric" to hide metrics, and the **garbage can icon** to remove metrics. +Use the **+ sign** to the right to add multiple metrics to your query. Click on the **eye icon** next to **Metric** to hide metrics, and the **garbage can icon** to remove metrics. - **Group by options** - Create multiple group by options when constructing your Elasticsearch query. Date histogram is the default option. Below is a list of options in the dropdown menu. From ed54239a9f6b8c0a77e53b3180c441e0c9a74203 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:50:50 +0200 Subject: [PATCH 022/180] Alerting: Dont show 1 firing series when no data in Expressions PreviewSummary (#76981) * Dont show 1 firing series when no data in Expressions PreviewSummary * Add comment to make clear we need to filter out undefineds for firing count * Move logic to a new getGroupedByStateAndSeriesCount method and added test to it --- .../expressions/Expression.test.tsx | 75 ++++++++++++++++++- .../components/expressions/Expression.tsx | 21 ++++-- 2 files changed, 89 insertions(+), 7 deletions(-) diff --git a/public/app/features/alerting/unified/components/expressions/Expression.test.tsx b/public/app/features/alerting/unified/components/expressions/Expression.test.tsx index 326cd6a7acd..3c502ac4370 100644 --- a/public/app/features/alerting/unified/components/expressions/Expression.test.tsx +++ b/public/app/features/alerting/unified/components/expressions/Expression.test.tsx @@ -1,11 +1,11 @@ -import { screen, render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { times } from 'lodash'; import React from 'react'; import { DataFrame, toDataFrame } from '@grafana/data'; -import { ExpressionResult } from './Expression'; +import { ExpressionResult, getGroupedByStateAndSeriesCount } from './Expression'; describe('TestResult', () => { it('should be able to render', () => { @@ -82,3 +82,74 @@ function makeSeries(n: number) { }) ); } + +describe('getGroupedByStateAndSeriesCount', () => { + it('should group series by state correctly and count the number of series', () => { + const series: DataFrame[] = [ + toDataFrame({ fields: [{ name: 'value', values: [1] }] }), + toDataFrame({ fields: [{ name: 'value', values: [undefined] }] }), + toDataFrame({ fields: [{ name: 'value', values: [0] }] }), + toDataFrame({ fields: [{ name: 'value', values: [2] }] }), + toDataFrame({ fields: [{ name: 'value', values: [undefined] }] }), + ]; + + const { groupedByState, seriesCount } = getGroupedByStateAndSeriesCount(series); + + expect(groupedByState['firing']).toEqual([series[0], series[3]]); + expect(groupedByState['inactive']).toEqual([series[2]]); + expect(seriesCount).toEqual(3); + }); + + it('should return empty group state and zero series count when input array is empty', () => { + const series: DataFrame[] = []; + + const { groupedByState, seriesCount } = getGroupedByStateAndSeriesCount(series); + + expect(groupedByState).toEqual({ + firing: [], + inactive: [], + }); + expect(seriesCount).toEqual(0); + }); + + it('should return zero series count and empty group state when all series have undefined values', () => { + const series = [ + toDataFrame({ fields: [{ name: 'value', values: [undefined] }] }), + toDataFrame({ fields: [{ name: 'value', values: [undefined] }] }), + ]; + + const { groupedByState, seriesCount } = getGroupedByStateAndSeriesCount(series); + + expect(groupedByState['firing']).toEqual([]); + expect(groupedByState['inactive']).toEqual([]); + expect(seriesCount).toEqual(0); + }); + + it('should group all series by inactive state when all series have zero values', () => { + const series = [ + toDataFrame({ fields: [{ name: 'value', values: [0] }] }), + toDataFrame({ fields: [{ name: 'value', values: [0] }] }), + toDataFrame({ fields: [{ name: 'value', values: [0] }] }), + ]; + + const { groupedByState, seriesCount } = getGroupedByStateAndSeriesCount(series); + + expect(groupedByState['firing']).toEqual([]); + expect(groupedByState['inactive']).toEqual(series); + expect(seriesCount).toEqual(series.length); + }); + + it('should group all series by Firing state when all series have non-zero values', () => { + const series = [ + toDataFrame({ fields: [{ name: 'value', values: [1] }] }), + toDataFrame({ fields: [{ name: 'value', values: [2] }] }), + toDataFrame({ fields: [{ name: 'value', values: [3] }] }), + ]; + + const { groupedByState, seriesCount } = getGroupedByStateAndSeriesCount(series); + + expect(groupedByState['firing']).toEqual(series); + expect(groupedByState['inactive']).toEqual([]); + expect(seriesCount).toEqual(series.length); + }); +}); diff --git a/public/app/features/alerting/unified/components/expressions/Expression.tsx b/public/app/features/alerting/unified/components/expressions/Expression.tsx index f6849d41a0a..efc5bbd6b8b 100644 --- a/public/app/features/alerting/unified/components/expressions/Expression.tsx +++ b/public/app/features/alerting/unified/components/expressions/Expression.tsx @@ -60,14 +60,10 @@ export const Expression: FC = ({ const isLoading = data && Object.values(data).some((d) => Boolean(d) && d.state === LoadingState.Loading); const hasResults = Array.isArray(data?.series) && !isLoading; const series = data?.series ?? []; - const seriesCount = series.length; const alertCondition = isAlertCondition ?? false; - const groupedByState = { - [PromAlertingRuleState.Firing]: series.filter((serie) => getSeriesValue(serie) !== 0), - [PromAlertingRuleState.Inactive]: series.filter((serie) => getSeriesValue(serie) === 0), - }; + const { seriesCount, groupedByState } = getGroupedByStateAndSeriesCount(series); const renderExpressionType = useCallback( (query: ExpressionQuery) => { @@ -236,6 +232,21 @@ export const PreviewSummary: FC<{ firing: number; normal: number; isCondition: b return {`${seriesCount} series`}; }; +export function getGroupedByStateAndSeriesCount(series: DataFrame[]) { + const noDataSeries = series.filter((serie) => getSeriesValue(serie) === undefined).length; + const groupedByState = { + // we need to filter out series with no data (undefined) or zero value + [PromAlertingRuleState.Firing]: series.filter( + (serie) => getSeriesValue(serie) !== undefined && getSeriesValue(serie) !== 0 + ), + [PromAlertingRuleState.Inactive]: series.filter((serie) => getSeriesValue(serie) === 0), + }; + + const seriesCount = series.length - noDataSeries; + + return { groupedByState, seriesCount }; +} + interface HeaderProps { refId: string; queryType: ExpressionQueryType; From 7a9ec6b4e0e3e8ebacc9207b5d45d0bb20d214b2 Mon Sep 17 00:00:00 2001 From: Ieva Date: Tue, 24 Oct 2023 14:51:12 +0100 Subject: [PATCH 023/180] RBAC: update data source permission API reference (#76613) * update data source permission docs * Update datasource_permissions.md * Update docs/sources/developers/http_api/datasource_permissions.md Co-authored-by: Gabriel MABILLE * Apply suggestions from code review --------- Co-authored-by: Gabriel MABILLE --- .../http_api/datasource_permissions.md | 341 ++++++++++-------- 1 file changed, 185 insertions(+), 156 deletions(-) diff --git a/docs/sources/developers/http_api/datasource_permissions.md b/docs/sources/developers/http_api/datasource_permissions.md index 16780c52fc9..9c0a0fc3ee8 100644 --- a/docs/sources/developers/http_api/datasource_permissions.md +++ b/docs/sources/developers/http_api/datasource_permissions.md @@ -27,124 +27,30 @@ title: Datasource Permissions HTTP API > If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "/docs/grafana/latest/administration/roles-and-permissions/access-control/custom-role-actions-scopes" >}}) for more information. -This API can be used to enable, disable, list, add and remove permissions for a data source. +This API can be used to list, add and remove permissions for a data source. -Permissions can be set for a user or a team. Permissions cannot be set for Admins - they always have access to everything. - -The permission levels for the permission field: - -- 1 = Query - -## Enable permissions for a data source - -`POST /api/datasources/:id/enable-permissions` - -Enables permissions for the data source with the given `id`. No one except Org Admins will be able to query the data source until permissions have been added which permit certain users or teams to query the data source. - -**Required permissions** - -See note in the [introduction]({{< ref "#data-source-permissions-api" >}}) for an explanation. - -| Action | Scope | -| ----------------------------- | ---------------------------------------------------------------------------- | -| datasources.permissions:write | datasources:\*
datasources:id:\*
datasources:id:1 (single data source) | - -### Examples - -**Example request:** - -```http -POST /api/datasources/1/enable-permissions -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - -{} -``` - -**Example response:** - -```http -HTTP/1.1 200 OK -Content-Type: application/json; charset=UTF-8 -Content-Length: 35 - -{"message":"Datasource permissions enabled"} -``` - -Status codes: - -- **200** - Ok -- **400** - Permissions cannot be enabled, see response body for details -- **401** - Unauthorized -- **403** - Access denied -- **404** - Datasource not found - -## Disable permissions for a data source - -`POST /api/datasources/:id/disable-permissions` - -Disables permissions for the data source with the given `id`. All existing permissions will be removed and anyone will be able to query the data source. - -**Required permissions** - -See note in the [introduction]({{< ref "#data-source-permissions-api" >}}) for an explanation. - -| Action | Scope | -| ----------------------------- | ---------------------------------------------------------------------------- | -| datasources.permissions:write | datasources:\*
datasources:id:\*
datasources:id:1 (single data source) | - -### Examples - -**Example request:** - -```http -POST /api/datasources/1/disable-permissions -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - -{} -``` - -**Example response:** - -```http -HTTP/1.1 200 OK -Content-Type: application/json; charset=UTF-8 -Content-Length: 35 - -{"message":"Datasource permissions disabled"} -``` - -Status codes: - -- **200** - Ok -- **400** - Permissions cannot be disabled, see response body for details -- **401** - Unauthorized -- **403** - Access denied -- **404** - Datasource not found +Permissions can be set for a user, team, service account or a basic role (Admin, Editor, Viewer). ## Get permissions for a data source -`GET /api/datasources/:id/permissions` +`GET /api/access-control/datasources/:uid` -Gets all existing permissions for the data source with the given `id`. +Gets all existing permissions for the data source with the given `uid`. **Required permissions** See note in the [introduction]({{< ref "#data-source-permissions-api" >}}) for an explanation. -| Action | Scope | -| ---------------------------- | ---------------------------------------------------------------------------- | -| datasources.permissions:read | datasources:\*
datasources:id:\*
datasources:id:1 (single data source) | +| Action | Scope | +| ---------------------------- | ------------------------------------------------------------------------------------------ | +| datasources.permissions:read | datasources:\*
datasources:uid:\*
datasources:uid:my_datasource (single data source) | ### Examples **Example request:** ```http -GET /api/datasources/1/permissions HTTP/1.1 +GET /api/access-control/datasources/my_datasource HTTP/1.1 Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk @@ -157,36 +63,57 @@ HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 Content-Length: 551 -{ - "datasourceId": 1, - "enabled": true, - "permissions": - [ +[ { - "id": 1, - "datasourceId": 1, - "userId": 1, - "userLogin": "user", - "userEmail": "user@test.com", - "userAvatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", - "permission": 1, - "permissionName": "Query", - "created": "2017-06-20T02:00:00+02:00", - "updated": "2017-06-20T02:00:00+02:00", + "id": 1, + "roleName": "fixed:datasources:reader", + "isManaged": false, + "isInherited": false, + "isServiceAccount": false, + "userId": 1, + "userLogin": "admin_user", + "userAvatarUrl": "/avatar/admin_user", + "actions": [ + "datasources:read", + "datasources:query", + "datasources:read", + "datasources:query", + "datasources:write", + "datasources:delete" + ], + "permission": "Edit" }, { - "id": 2, - "datasourceId": 1, - "teamId": 1, - "team": "A Team", - "teamAvatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", - "permission": 1, - "permissionName": "Query", - "created": "2017-06-20T02:00:00+02:00", - "updated": "2017-06-20T02:00:00+02:00", - } - ] -} + "id": 2, + "roleName": "managed:teams:1:permissions", + "isManaged": true, + "isInherited": false, + "isServiceAccount": false, + "team": "A team", + "teamId": 1, + "teamAvatarUrl": "/avatar/523d70c8551046f441727d690431858c", + "actions": [ + "datasources:read", + "datasources:query" + ], + "permission": "Query" + }, + { + "id": 3, + "roleName": "basic:admin", + "isManaged": false, + "isInherited": false, + "isServiceAccount": false, + "builtInRole": "Admin", + "actions": [ + "datasources:query", + "datasources:read", + "datasources:write", + "datasources:delete" + ], + "permission": "Edit" + }, +] ``` Status codes: @@ -194,35 +121,37 @@ Status codes: - **200** - Ok - **401** - Unauthorized - **403** - Access denied -- **404** - Datasource not found +- **500** - Internal error -## Add permission for a data source +## Add or revoke access to a data source for a user -`POST /api/datasources/:id/permissions` +`POST /api/access-control/datasources/:uid/users/:id` -Adds a user permission for the data source with the given `id`. +Sets user permission for the data source with the given `uid`. + +To add a permission, set the `permission` field to either `Query`, `Edit`, or `Admin`. +To remove a permission, set the `permission` field to an empty string. **Required permissions** See note in the [introduction]({{< ref "#data-source-permissions-api" >}}) for an explanation. -| Action | Scope | -| ----------------------------- | ---------------------------------------------------------------------------- | -| datasources.permissions:write | datasources:\*
datasources:id:\*
datasources:id:1 (single data source) | +| Action | Scope | +| ----------------------------- | ------------------------------------------------------------------------------------------ | +| datasources.permissions:write | datasources:\*
datasources:uid:\*
datasources:uid:my_datasource (single data source) | ### Examples **Example request:** ```http -POST /api/datasources/1/permissions +POST /api/access-control/datasources/my_datasource/users/1 Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk { - "userId": 1, - "permission": 1 + "permission": "Query", } ``` @@ -233,22 +162,19 @@ HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 Content-Length: 35 -{"message":"Datasource permission added"} +{"message": "Permission updated"} ``` -Adds a team permission for the data source with the given `id`. - **Example request:** ```http -POST /api/datasources/1/permissions +POST /api/access-control/datasources/my_datasource/users/1 Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk { - "teamId": 1, - "permission": 1 + "permission": "", } ``` @@ -259,7 +185,7 @@ HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 Content-Length: 35 -{"message":"Datasource permission added"} +{"message": "Permission removed"} ``` Status codes: @@ -268,31 +194,37 @@ Status codes: - **400** - Permission cannot be added, see response body for details - **401** - Unauthorized - **403** - Access denied -- **404** - Datasource not found -## Remove permission for a data source +## Add or revoke access to a data source for a team -`DELETE /api/datasources/:id/permissions/:permissionId` +`POST /api/access-control/datasources/:uid/teams/:id` -Removes the permission with the given `permissionId` for the data source with the given `id`. +Sets team permission for the data source with the given `uid`. + +To add a permission, set the `permission` field to either `Query`, `Edit`, or `Admin`. +To remove a permission, set the `permission` field to an empty string. **Required permissions** See note in the [introduction]({{< ref "#data-source-permissions-api" >}}) for an explanation. -| Action | Scope | -| ----------------------------- | ---------------------------------------------------------------------------- | -| datasources.permissions:write | datasources:\*
datasources:id:\*
datasources:id:1 (single data source) | +| Action | Scope | +| ----------------------------- | ------------------------------------------------------------------------------------------ | +| datasources.permissions:write | datasources:\*
datasources:uid:\*
datasources:uid:my_datasource (single data source) | ### Examples **Example request:** ```http -DELETE /api/datasources/1/permissions/2 +POST /api/access-control/datasources/my_datasource/teams/1 Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "permission": "Edit", +} ``` **Example response:** @@ -302,12 +234,109 @@ HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 Content-Length: 35 -{"message":"Datasource permission removed"} +{"message": "Permission updated"} +``` + +**Example request:** + +```http +POST /api/access-control/datasources/my_datasource/teams/1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "permission": "", +} +``` + +**Example response:** + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{"message": "Permission removed"} ``` Status codes: - **200** - Ok +- **400** - Permission cannot be added, see response body for details +- **401** - Unauthorized +- **403** - Access denied + +## Add or revoke access to a data source for a basic role + +`POST /api/access-control/datasources/:uid/builtInRoles/:builtinRoleName` + +Sets permission for the data source with the given `uid` to all users who have the specified basic role. + +You can set permissions for the following basic roles: `Admin`, `Editor`, `Viewer`. + +To add a permission, set the `permission` field to either `Query`, `Edit`, or `Admin`. +To remove a permission, set the `permission` field to an empty string. + +**Required permissions** + +See note in the [introduction]({{< ref "#data-source-permissions-api" >}}) for an explanation. + +| Action | Scope | +| ----------------------------- | ------------------------------------------------------------------------------------------ | +| datasources.permissions:write | datasources:\*
datasources:uid:\*
datasources:uid:my_datasource (single data source) | + +### Examples + +**Example request:** + +```http +POST /api/access-control/datasources/my_datasource/builtInRoles/Admin +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "permission": "Edit", +} +``` + +**Example response:** + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{"message": "Permission updated"} +``` + +**Example request:** + +```http +POST /api/access-control/datasources/my_datasource/builtInRoles/Viewer +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "permission": "", +} +``` + +**Example response:** + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{"message": "Permission removed"} +``` + +Status codes: + +- **200** - Ok +- **400** - Permission cannot be added, see response body for details - **401** - Unauthorized - **403** - Access denied -- **404** - Datasource not found or permission not found From 07909464f11afde928d81de16c1d3e69baa251f2 Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Tue, 24 Oct 2023 14:52:41 +0100 Subject: [PATCH 024/180] Update `doc-validator` workflow (#77024) Update doc-validator No longer produce errors for the use of https://grafana.com/ links. This is the first step towards just using fully qualified URLs everywhere. The website link render-hook will internally transform these URLs into the partial URL that works across all hostnames. Signed-off-by: Jack Baldry --- .github/workflows/doc-validator.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/doc-validator.yml b/.github/workflows/doc-validator.yml index 33987319442..9e28a128b97 100644 --- a/.github/workflows/doc-validator.yml +++ b/.github/workflows/doc-validator.yml @@ -7,7 +7,7 @@ jobs: doc-validator: runs-on: "ubuntu-latest" container: - image: "grafana/doc-validator:v3.2.1" + image: "grafana/doc-validator:v4.0.0" steps: - name: "Checkout code" uses: "actions/checkout@v4" From 897e3a4dab5e30bff9bc35ea5a8ad0115f5649dc Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Tue, 24 Oct 2023 15:54:14 +0200 Subject: [PATCH 025/180] AuthN: Add metrics to external service accounts management (#76789) * AuthN: Add metrics to external service accounts management * Add a new metric to count stored external service accounts * Update variable names Co-authored-by: linoman <2051016+linoman@users.noreply.github.com> * Add test to SearchOrgServiceAccounts * Add feature flags checks before registering and using the metrics --------- Co-authored-by: linoman <2051016+linoman@users.noreply.github.com> --- .../serviceaccounts/database/store.go | 46 ++++--- .../serviceaccounts/database/store_test.go | 127 ++++++++++++++++++ .../serviceaccounts/extsvcaccounts/metrics.go | 68 ++++++++++ .../serviceaccounts/extsvcaccounts/models.go | 2 + .../serviceaccounts/extsvcaccounts/service.go | 31 ++++- .../extsvcaccounts/service_test.go | 10 +- pkg/services/serviceaccounts/models.go | 4 + pkg/services/serviceaccounts/proxy/service.go | 12 ++ .../serviceaccounts/proxy/service_test.go | 38 +++++- .../serviceaccounts/serviceaccounts.go | 7 +- pkg/services/serviceaccounts/tests/common.go | 32 +++++ pkg/services/serviceaccounts/tests/mocks.go | 5 + 12 files changed, 352 insertions(+), 30 deletions(-) create mode 100644 pkg/services/serviceaccounts/extsvcaccounts/metrics.go diff --git a/pkg/services/serviceaccounts/database/store.go b/pkg/services/serviceaccounts/database/store.go index 292719cedc5..5790b755baa 100644 --- a/pkg/services/serviceaccounts/database/store.go +++ b/pkg/services/serviceaccounts/database/store.go @@ -270,9 +270,6 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context, } err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error { - sess := dbSession.Table("org_user") - sess.Join("INNER", s.sqlStore.GetDialect().Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", s.sqlStore.GetDialect().Quote("user"))) - whereConditions := make([]string, 0) whereParams := make([]any, 0) @@ -312,10 +309,38 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context, whereConditions, "is_disabled = ?") whereParams = append(whereParams, s.sqlStore.GetDialect().BooleanStr(true)) + case serviceaccounts.FilterOnlyExternal: + whereConditions = append( + whereConditions, + "login "+s.sqlStore.GetDialect().LikeStr()+" ?") + whereParams = append(whereParams, serviceaccounts.ServiceAccountPrefix+serviceaccounts.ExtSvcPrefix+"%") default: s.log.Warn("Invalid filter user for service account filtering", "service account search filtering", query.Filter) } + // Count the number of accounts + serviceaccount := serviceaccounts.ServiceAccountDTO{} + countSess := dbSession.Table("org_user") + countSess.Join("INNER", s.sqlStore.GetDialect().Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", s.sqlStore.GetDialect().Quote("user"))) + + if len(whereConditions) > 0 { + countSess.Where(strings.Join(whereConditions, " AND "), whereParams...) + } + count, err := countSess.Count(&serviceaccount) + if err != nil { + return err + } + searchResult.TotalCount = count + + // Stop here if we only wanted to count the number of accounts + if query.CountOnly { + return nil + } + + // Fetch service accounts + sess := dbSession.Table("org_user") + sess.Join("INNER", s.sqlStore.GetDialect().Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", s.sqlStore.GetDialect().Quote("user"))) + if len(whereConditions) > 0 { sess.Where(strings.Join(whereConditions, " AND "), whereParams...) } @@ -338,21 +363,6 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context, if err := sess.Find(&searchResult.ServiceAccounts); err != nil { return err } - - // get total - serviceaccount := serviceaccounts.ServiceAccountDTO{} - countSess := dbSession.Table("org_user") - sess.Join("INNER", s.sqlStore.GetDialect().Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", s.sqlStore.GetDialect().Quote("user"))) - - if len(whereConditions) > 0 { - countSess.Where(strings.Join(whereConditions, " AND "), whereParams...) - } - count, err := countSess.Count(&serviceaccount) - if err != nil { - return err - } - searchResult.TotalCount = count - return nil }) if err != nil { diff --git a/pkg/services/serviceaccounts/database/store_test.go b/pkg/services/serviceaccounts/database/store_test.go index 11bf66caa72..e9e11d5f3ba 100644 --- a/pkg/services/serviceaccounts/database/store_test.go +++ b/pkg/services/serviceaccounts/database/store_test.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/kvstore" + ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apikey/apikeyimpl" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" @@ -368,3 +369,129 @@ func TestStore_MigrateAllApiKeys(t *testing.T) { }) } } +func TestServiceAccountsStoreImpl_SearchOrgServiceAccounts(t *testing.T) { + initUsers := []tests.TestUser{ + {Name: "satest-1", Role: string(org.RoleViewer), Login: "sa-satest-1", IsServiceAccount: true}, + {Name: "usertest-2", Role: string(org.RoleEditor), Login: "usertest-2", IsServiceAccount: false}, + {Name: "satest-3", Role: string(org.RoleEditor), Login: "sa-satest-3", IsServiceAccount: true}, + {Name: "satest-4", Role: string(org.RoleAdmin), Login: "sa-satest-4", IsServiceAccount: true}, + {Name: "extsvc-test-5", Role: string(org.RoleNone), Login: "sa-extsvc-test-5", IsServiceAccount: true}, + {Name: "extsvc-test-6", Role: string(org.RoleNone), Login: "sa-extsvc-test-6", IsServiceAccount: true}, + {Name: "extsvc-test-7", Role: string(org.RoleNone), Login: "sa-extsvc-test-7", IsServiceAccount: true}, + {Name: "extsvc-test-8", Role: string(org.RoleNone), Login: "sa-extsvc-test-8", IsServiceAccount: true}, + } + + db, store := setupTestDatabase(t) + orgID := tests.SetupUsersServiceAccounts(t, db, initUsers) + + userWithPerm := &user.SignedInUser{ + OrgID: orgID, + Permissions: map[int64]map[string][]string{orgID: {serviceaccounts.ActionRead: {serviceaccounts.ScopeAll}}}, + } + + tt := []struct { + desc string + query *serviceaccounts.SearchOrgServiceAccountsQuery + expectedTotal int64 // Value of the result.TotalCount + expectedCount int // Length of the result.ServiceAccounts slice + expectedErr error + }{ + { + desc: "should list all service accounts", + query: &serviceaccounts.SearchOrgServiceAccountsQuery{ + OrgID: orgID, + SignedInUser: userWithPerm, + Filter: serviceaccounts.FilterIncludeAll, + }, + expectedTotal: 7, + expectedCount: 7, + }, + { + desc: "should list no service accounts without permissions", + query: &serviceaccounts.SearchOrgServiceAccountsQuery{ + OrgID: orgID, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: map[int64]map[string][]string{orgID: {}}, + }, + Filter: serviceaccounts.FilterIncludeAll, + }, + expectedTotal: 0, + expectedCount: 0, + }, + { + desc: "should list one service accounts with restricted permissions", + query: &serviceaccounts.SearchOrgServiceAccountsQuery{ + OrgID: orgID, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: map[int64]map[string][]string{orgID: {serviceaccounts.ActionRead: { + ac.Scope("serviceaccounts", "id", "1"), + ac.Scope("serviceaccounts", "id", "7"), + }}}, + }, + Filter: serviceaccounts.FilterIncludeAll, + }, + expectedTotal: 2, + expectedCount: 2, + }, + { + desc: "should list only external service accounts", + query: &serviceaccounts.SearchOrgServiceAccountsQuery{ + OrgID: orgID, + SignedInUser: userWithPerm, + Filter: serviceaccounts.FilterOnlyExternal, + }, + expectedTotal: 4, + expectedCount: 4, + }, + { + desc: "should return service accounts with sa-satest login", + query: &serviceaccounts.SearchOrgServiceAccountsQuery{ + OrgID: orgID, + Query: "sa-satest", + SignedInUser: userWithPerm, + Filter: serviceaccounts.FilterIncludeAll, + }, + expectedTotal: 3, + expectedCount: 3, + }, + { + desc: "should only count service accounts", + query: &serviceaccounts.SearchOrgServiceAccountsQuery{ + OrgID: orgID, + SignedInUser: userWithPerm, + Filter: serviceaccounts.FilterIncludeAll, + CountOnly: true, + }, + expectedTotal: 7, + expectedCount: 0, + }, + { + desc: "should paginate result", + query: &serviceaccounts.SearchOrgServiceAccountsQuery{ + OrgID: orgID, + Page: 4, + Limit: 2, + SignedInUser: userWithPerm, + Filter: serviceaccounts.FilterIncludeAll, + }, + expectedTotal: 7, + expectedCount: 1, + }, + } + for _, tc := range tt { + t.Run(tc.desc, func(t *testing.T) { + ctx := context.Background() + + got, err := store.SearchOrgServiceAccounts(ctx, tc.query) + if tc.expectedErr != nil { + require.ErrorIs(t, err, tc.expectedErr) + return + } + + require.Equal(t, tc.expectedTotal, got.TotalCount) + require.Len(t, got.ServiceAccounts, tc.expectedCount) + }) + } +} diff --git a/pkg/services/serviceaccounts/extsvcaccounts/metrics.go b/pkg/services/serviceaccounts/extsvcaccounts/metrics.go new file mode 100644 index 00000000000..b0da11e64f1 --- /dev/null +++ b/pkg/services/serviceaccounts/extsvcaccounts/metrics.go @@ -0,0 +1,68 @@ +package extsvcaccounts + +import ( + "context" + "time" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/extsvcauth" + "github.com/grafana/grafana/pkg/services/serviceaccounts" + "github.com/grafana/grafana/pkg/services/user" + "github.com/prometheus/client_golang/prometheus" +) + +type metrics struct { + storedCount prometheus.GaugeFunc + savedCount prometheus.Counter + deletedCount prometheus.Counter +} + +func newMetrics(reg prometheus.Registerer, saSvc serviceaccounts.Service, logger log.Logger) *metrics { + var m metrics + + m.storedCount = prometheus.NewGaugeFunc( + prometheus.GaugeOpts{ + Namespace: metricsNamespace, + Name: "extsvc_total", + Help: "Number of external service accounts in store", + }, + func() float64 { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + res, err := saSvc.SearchOrgServiceAccounts(ctx, &serviceaccounts.SearchOrgServiceAccountsQuery{ + OrgID: extsvcauth.TmpOrgID, + Filter: serviceaccounts.FilterOnlyExternal, + CountOnly: true, + SignedInUser: &user.SignedInUser{ + OrgID: extsvcauth.TmpOrgID, + Permissions: map[int64]map[string][]string{ + extsvcauth.TmpOrgID: {serviceaccounts.ActionRead: {"serviceaccounts:id:*"}}, + }, + }, + }) + if err != nil { + logger.Error("Could not compute extsvc_total metric", "error", err) + return 0.0 + } + return float64(res.TotalCount) + }, + ) + m.savedCount = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: metricsNamespace, + Name: "extsvc_saved_total", + Help: "Number of external service accounts saved since start up.", + }) + m.deletedCount = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: metricsNamespace, + Name: "extsvc_deleted_total", + Help: "Number of external service accounts deleted since start up.", + }) + + if reg != nil { + reg.MustRegister(m.storedCount) + reg.MustRegister(m.savedCount) + reg.MustRegister(m.deletedCount) + } + + return &m +} diff --git a/pkg/services/serviceaccounts/extsvcaccounts/models.go b/pkg/services/serviceaccounts/extsvcaccounts/models.go index 8ea0cb77d36..3485d0289a4 100644 --- a/pkg/services/serviceaccounts/extsvcaccounts/models.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/models.go @@ -7,6 +7,8 @@ import ( ) const ( + metricsNamespace = "grafana" + kvStoreType = "extsvc-token" // #nosec G101 - this is not a hardcoded secret tokenNamePrefix = "extsvc-token" diff --git a/pkg/services/serviceaccounts/extsvcaccounts/service.go b/pkg/services/serviceaccounts/extsvcaccounts/service.go index 6eed6b6e978..ae29ccaedab 100644 --- a/pkg/services/serviceaccounts/extsvcaccounts/service.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/service.go @@ -4,6 +4,8 @@ import ( "context" "errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/components/satokengen" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" @@ -11,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/models/roletype" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/extsvcauth" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/kvstore" sa "github.com/grafana/grafana/pkg/services/serviceaccounts" @@ -19,19 +22,29 @@ import ( type ExtSvcAccountsService struct { acSvc ac.Service + features *featuremgmt.FeatureManager logger log.Logger + metrics *metrics saSvc sa.Service skvStore kvstore.SecretsKVStore } -func ProvideExtSvcAccountsService(acSvc ac.Service, saSvc *manager.ServiceAccountsService, db db.DB, secretsSvc secrets.Service) *ExtSvcAccountsService { +func ProvideExtSvcAccountsService(acSvc ac.Service, db db.DB, features *featuremgmt.FeatureManager, reg prometheus.Registerer, saSvc *manager.ServiceAccountsService, secretsSvc secrets.Service) *ExtSvcAccountsService { logger := log.New("serviceauth.extsvcaccounts") - return &ExtSvcAccountsService{ + esa := &ExtSvcAccountsService{ acSvc: acSvc, logger: logger, saSvc: saSvc, + features: features, skvStore: kvstore.NewSQLSecretsKVStore(db, secretsSvc, logger), // Using SQL store to avoid a cyclic dependency } + + // Register the metrics + if features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) || features.IsEnabled(featuremgmt.FlagExternalServiceAuth) { + esa.metrics = newMetrics(reg, saSvc, logger) + } + + return esa } // RetrieveExtSvcAccount fetches an external service account by ID @@ -52,6 +65,12 @@ func (esa *ExtSvcAccountsService) RetrieveExtSvcAccount(ctx context.Context, org // SaveExternalService creates, updates or delete a service account (and its token) with the requested permissions. func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error) { + // This is double proofing, we should never reach here anyway the flags have already been checked. + if !esa.features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) && !esa.features.IsEnabled(featuremgmt.FlagExternalServiceAuth) { + esa.logger.Warn("This feature is behind a feature flag, please set it if you want to save external services") + return nil, nil + } + if cmd == nil { esa.logger.Warn("Received no input") return nil, nil @@ -92,6 +111,12 @@ func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd * // ManageExtSvcAccount creates, updates or deletes the service account associated with an external service func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *sa.ManageExtSvcAccountCmd) (int64, error) { + // This is double proofing, we should never reach here anyway the flags have already been checked. + if !esa.features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) && !esa.features.IsEnabled(featuremgmt.FlagExternalServiceAuth) { + esa.logger.Warn("This feature is behind a feature flag, please set it if you want to save external services") + return 0, nil + } + if cmd == nil { esa.logger.Warn("Received no input") return 0, nil @@ -111,6 +136,7 @@ func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd * "error", err.Error()) return 0, err } + esa.metrics.deletedCount.Inc() } esa.logger.Info("Skipping service account creation", "service", cmd.ExtSvcSlug, @@ -130,6 +156,7 @@ func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd * esa.logger.Error("Could not save service account", "service", cmd.ExtSvcSlug, "error", errSave.Error()) return 0, errSave } + esa.metrics.savedCount.Inc() return saID, nil } diff --git a/pkg/services/serviceaccounts/extsvcaccounts/service_test.go b/pkg/services/serviceaccounts/extsvcaccounts/service_test.go index dc2034786bc..4adfd06f3ae 100644 --- a/pkg/services/serviceaccounts/extsvcaccounts/service_test.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/service_test.go @@ -4,6 +4,9 @@ import ( "context" "testing" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models/roletype" @@ -17,8 +20,6 @@ import ( sa "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" ) type TestEnv struct { @@ -39,9 +40,12 @@ func setupTestEnv(t *testing.T) *TestEnv { SaSvc: &tests.MockServiceAccountService{}, SkvStore: kvstore.NewFakeSecretsKVStore(), } + logger := log.New("extsvcaccounts.test") env.S = &ExtSvcAccountsService{ acSvc: acimpl.ProvideOSSService(cfg, env.AcStore, localcache.New(0, 0), fmgt), - logger: log.New("extsvcaccounts.test"), + features: fmgt, + logger: logger, + metrics: newMetrics(nil, env.SaSvc, logger), saSvc: env.SaSvc, skvStore: env.SkvStore, } diff --git a/pkg/services/serviceaccounts/models.go b/pkg/services/serviceaccounts/models.go index 6346cd6cd7c..595818bc0e0 100644 --- a/pkg/services/serviceaccounts/models.go +++ b/pkg/services/serviceaccounts/models.go @@ -84,6 +84,8 @@ type ServiceAccountDTO struct { OrgId int64 `json:"orgId" xorm:"org_id"` // example: false IsDisabled bool `json:"isDisabled" xorm:"is_disabled"` + // example: false + IsExternal bool `json:"isExternal,omitempty" xorm:"-"` // example: Viewer Role string `json:"role" xorm:"role"` // example: 0 @@ -112,6 +114,7 @@ type SearchOrgServiceAccountsQuery struct { Filter ServiceAccountFilter Page int Limit int + CountOnly bool SignedInUser identity.Requester } @@ -166,6 +169,7 @@ const ( FilterOnlyExpiredTokens ServiceAccountFilter = "expiredTokens" FilterOnlyDisabled ServiceAccountFilter = "disabled" FilterIncludeAll ServiceAccountFilter = "all" + FilterOnlyExternal ServiceAccountFilter = "external" ) type Stats struct { diff --git a/pkg/services/serviceaccounts/proxy/service.go b/pkg/services/serviceaccounts/proxy/service.go index 95e79cc1a28..d6bda2a2452 100644 --- a/pkg/services/serviceaccounts/proxy/service.go +++ b/pkg/services/serviceaccounts/proxy/service.go @@ -100,6 +100,18 @@ func (s *ServiceAccountsProxy) UpdateServiceAccount(ctx context.Context, orgID, return s.proxiedService.UpdateServiceAccount(ctx, orgID, serviceAccountID, saForm) } +func (s *ServiceAccountsProxy) SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) { + sa, err := s.proxiedService.SearchOrgServiceAccounts(ctx, query) + if err != nil { + return nil, err + } + + for i := range sa.ServiceAccounts { + sa.ServiceAccounts[i].IsExternal = isExternalServiceAccount(sa.ServiceAccounts[i].Login) + } + return sa, nil +} + func isNameValid(name string) bool { return !strings.HasPrefix(name, serviceaccounts.ExtSvcPrefix) } diff --git a/pkg/services/serviceaccounts/proxy/service_test.go b/pkg/services/serviceaccounts/proxy/service_test.go index a1258b80369..e9bc7461618 100644 --- a/pkg/services/serviceaccounts/proxy/service_test.go +++ b/pkg/services/serviceaccounts/proxy/service_test.go @@ -4,15 +4,18 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/extsvcaccounts" - "github.com/stretchr/testify/assert" ) type FakeServiceAccountsService struct { - ExpectedServiceAccountProfileDTO *serviceaccounts.ServiceAccountProfileDTO + ExpectedServiceAccountProfileDTO *serviceaccounts.ServiceAccountProfileDTO + ExpectedSearchOrgServiceAccountsResult *serviceaccounts.SearchOrgServiceAccountsResult } var _ serviceaccounts.Service = (*FakeServiceAccountsService)(nil) @@ -47,7 +50,11 @@ func (f *FakeServiceAccountsService) AddServiceAccountToken(ctx context.Context, return nil, nil } -func TestProvideServiceAccount_DeleteServiceAccount(t *testing.T) { +func (f *FakeServiceAccountsService) SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) { + return f.ExpectedSearchOrgServiceAccountsResult, nil +} + +func TestProvideServiceAccount_crudServiceAccount(t *testing.T) { testOrgId := int64(1) testServiceAccountId := int64(1) serviceMock := newServiceAccountServiceFake() @@ -257,3 +264,28 @@ func TestProvideServiceAccount_DeleteServiceAccount(t *testing.T) { assert.True(t, isExternalServiceAccount("sa-extsvc-my-service-account")) }) } + +func TestProvideServiceAccount_SearchServiceAccount(t *testing.T) { + serviceMock := newServiceAccountServiceFake() + svc := ServiceAccountsProxy{ + log.New("test"), + serviceMock, + } + + t.Run("should mark external service accounts correctly", func(t *testing.T) { + serviceMock.ExpectedSearchOrgServiceAccountsResult = &serviceaccounts.SearchOrgServiceAccountsResult{ + TotalCount: 2, + ServiceAccounts: []*serviceaccounts.ServiceAccountDTO{ + {Login: "test"}, + {Login: serviceaccounts.ServiceAccountPrefix + serviceaccounts.ExtSvcPrefix + "test"}, + }, + Page: 1, + PerPage: 2, + } + res, err := svc.SearchOrgServiceAccounts(context.Background(), &serviceaccounts.SearchOrgServiceAccountsQuery{OrgID: 1}) + require.Len(t, res.ServiceAccounts, 2) + require.NoError(t, err) + require.False(t, res.ServiceAccounts[0].IsExternal) + require.True(t, res.ServiceAccounts[1].IsExternal) + }) +} diff --git a/pkg/services/serviceaccounts/serviceaccounts.go b/pkg/services/serviceaccounts/serviceaccounts.go index f7ab7e25b3f..ed889c47475 100644 --- a/pkg/services/serviceaccounts/serviceaccounts.go +++ b/pkg/services/serviceaccounts/serviceaccounts.go @@ -13,14 +13,13 @@ Service accounts are used to authenticate API requests. They are not users and do not have a password. */ type Service interface { + AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *AddServiceAccountTokenCommand) (*apikey.APIKey, error) CreateServiceAccount(ctx context.Context, orgID int64, saForm *CreateServiceAccountForm) (*ServiceAccountDTO, error) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*ServiceAccountProfileDTO, error) RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) - UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, - saForm *UpdateServiceAccountForm) (*ServiceAccountProfileDTO, error) - AddServiceAccountToken(ctx context.Context, serviceAccountID int64, - cmd *AddServiceAccountTokenCommand) (*apikey.APIKey, error) + SearchOrgServiceAccounts(ctx context.Context, query *SearchOrgServiceAccountsQuery) (*SearchOrgServiceAccountsResult, error) + UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, saForm *UpdateServiceAccountForm) (*ServiceAccountProfileDTO, error) } //go:generate mockery --name ExtSvcAccountsService --structname MockExtSvcAccountsService --output tests --outpkg tests --filename extsvcaccmock.go diff --git a/pkg/services/serviceaccounts/tests/common.go b/pkg/services/serviceaccounts/tests/common.go index b10fb4e8a33..f559f128f05 100644 --- a/pkg/services/serviceaccounts/tests/common.go +++ b/pkg/services/serviceaccounts/tests/common.go @@ -102,3 +102,35 @@ func SetupApiKey(t *testing.T, sqlStore *sqlstore.SQLStore, testKey TestApiKey) return key } + +// SetupUsersServiceAccounts creates in "test org" all users or service accounts passed in parameter +// To achieve this, it sets the AutoAssignOrg and AutoAssignOrgId settings. +func SetupUsersServiceAccounts(t *testing.T, sqlStore *sqlstore.SQLStore, testUsers []TestUser) (orgID int64) { + role := string(org.RoleNone) + + quotaService := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg) + orgService, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sqlStore, orgService, sqlStore.Cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + require.NoError(t, err) + + org, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{ + Name: "test org", + }) + require.NoError(t, err) + + sqlStore.Cfg.AutoAssignOrg = true + sqlStore.Cfg.AutoAssignOrgId = int(org.ID) + + for i := range testUsers { + _, err := usrSvc.Create(context.Background(), &user.CreateUserCommand{ + Login: testUsers[i].Login, + IsServiceAccount: testUsers[i].IsServiceAccount, + DefaultOrgRole: role, + Name: testUsers[i].Name, + OrgID: org.ID, + }) + require.NoError(t, err) + } + return org.ID +} diff --git a/pkg/services/serviceaccounts/tests/mocks.go b/pkg/services/serviceaccounts/tests/mocks.go index 0d06a560e2b..d7b5a30d03e 100644 --- a/pkg/services/serviceaccounts/tests/mocks.go +++ b/pkg/services/serviceaccounts/tests/mocks.go @@ -50,3 +50,8 @@ func (s *MockServiceAccountService) UpdateServiceAccount(ctx context.Context, or mockedArgs := s.Called(ctx, orgID, serviceAccountID) return mockedArgs.Get(0).(*serviceaccounts.ServiceAccountProfileDTO), mockedArgs.Error(1) } + +func (s *MockServiceAccountService) SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) { + mockedArgs := s.Called(ctx, query) + return mockedArgs.Get(0).(*serviceaccounts.SearchOrgServiceAccountsResult), mockedArgs.Error(1) +} From 0e2fb5649f3fa292cbb8c5e5307f4b470dd05221 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Tue, 24 Oct 2023 16:09:19 +0200 Subject: [PATCH 026/180] Revert "Chore: Bump Lerna to v7" (#77046) Revert "Chore: Bump Lerna to v7 (#76851)" This reverts commit c89b49bacbee0ea08801353a465b44ca22d8bda7. --- lerna.json | 2 + package.json | 2 +- yarn.lock | 2765 +++++++++++++++++++++++++++++--------------------- 3 files changed, 1619 insertions(+), 1150 deletions(-) diff --git a/lerna.json b/lerna.json index 636d8d0046a..5f419bdcc36 100644 --- a/lerna.json +++ b/lerna.json @@ -1,4 +1,6 @@ { "npmClient": "yarn", + "useWorkspaces": true, + "packages": ["packages/*"], "version": "10.3.0-pre" } diff --git a/package.json b/package.json index 87a17ef6268..776f96d4b78 100644 --- a/package.json +++ b/package.json @@ -198,7 +198,7 @@ "jest-fail-on-console": "3.1.1", "jest-junit": "16.0.0", "jest-matcher-utils": "29.7.0", - "lerna": "7.4.1", + "lerna": "5.5.4", "mini-css-extract-plugin": "2.7.6", "msw": "1.3.2", "mutationobserver-shim": "0.3.7", diff --git a/yarn.lock b/yarn.lock index 9dfb0bdfc56..4a01ddcbb86 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3616,6 +3616,13 @@ __metadata: languageName: node linkType: hard +"@isaacs/string-locale-compare@npm:^1.1.0": + version: 1.1.0 + resolution: "@isaacs/string-locale-compare@npm:1.1.0" + checksum: 7287da5d11497b82c542d3c2abe534808015be4f4883e71c26853277b5456f6bbe4108535db847a29f385ad6dc9318ffb0f55ee79bb5f39993233d7dccf8751d + languageName: node + linkType: hard + "@istanbuljs/load-nyc-config@npm:^1.0.0": version: 1.1.0 resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" @@ -4026,87 +4033,807 @@ __metadata: languageName: node linkType: hard -"@lerna/child-process@npm:7.4.1": - version: 7.4.1 - resolution: "@lerna/child-process@npm:7.4.1" +"@lerna/add@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/add@npm:5.5.4" + dependencies: + "@lerna/bootstrap": 5.5.4 + "@lerna/command": 5.5.4 + "@lerna/filter-options": 5.5.4 + "@lerna/npm-conf": 5.5.4 + "@lerna/validation-error": 5.5.4 + dedent: ^0.7.0 + npm-package-arg: 8.1.1 + p-map: ^4.0.0 + pacote: ^13.6.1 + semver: ^7.3.4 + checksum: f4f17fda326a550cdbb3025a98a5ccf0e275b378fb1a1df6f518b5cbd3f334d5486394f84d12adddc8341d2802a37715390fdbf71375327dc89bdcd4986ef364 + languageName: node + linkType: hard + +"@lerna/bootstrap@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/bootstrap@npm:5.5.4" + dependencies: + "@lerna/command": 5.5.4 + "@lerna/filter-options": 5.5.4 + "@lerna/has-npm-version": 5.5.4 + "@lerna/npm-install": 5.5.4 + "@lerna/package-graph": 5.5.4 + "@lerna/pulse-till-done": 5.5.4 + "@lerna/rimraf-dir": 5.5.4 + "@lerna/run-lifecycle": 5.5.4 + "@lerna/run-topologically": 5.5.4 + "@lerna/symlink-binary": 5.5.4 + "@lerna/symlink-dependencies": 5.5.4 + "@lerna/validation-error": 5.5.4 + "@npmcli/arborist": 5.3.0 + dedent: ^0.7.0 + get-port: ^5.1.1 + multimatch: ^5.0.0 + npm-package-arg: 8.1.1 + npmlog: ^6.0.2 + p-map: ^4.0.0 + p-map-series: ^2.1.0 + p-waterfall: ^2.1.1 + semver: ^7.3.4 + checksum: 67a5f30045690b2b62be901c0272f6a67d830ca7183f1296d1b551a1d25c4e13ed0c1e6286a682685e42e2b6d5cd421dd16e812c12cefc43dd62036376fe4230 + languageName: node + linkType: hard + +"@lerna/changed@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/changed@npm:5.5.4" + dependencies: + "@lerna/collect-updates": 5.5.4 + "@lerna/command": 5.5.4 + "@lerna/listable": 5.5.4 + "@lerna/output": 5.5.4 + checksum: f2ff13b2a00740832428cfc626ae559c1cd210e8a6e71ee489c6872942c62cdec252598180bb3bada96d4ef6bc3ae494b5bb83b1b03e5313df96c52b4daf5e0d + languageName: node + linkType: hard + +"@lerna/check-working-tree@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/check-working-tree@npm:5.5.4" + dependencies: + "@lerna/collect-uncommitted": 5.5.4 + "@lerna/describe-ref": 5.5.4 + "@lerna/validation-error": 5.5.4 + checksum: 43d28c714b96ddf6d7cd9023f0f24a32786420d40746037a3cf4e35d03441ba4c3760826034034692ac355091d2a8a5166ee5538833762db51301982f732abaa + languageName: node + linkType: hard + +"@lerna/child-process@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/child-process@npm:5.5.4" dependencies: chalk: ^4.1.0 execa: ^5.0.0 strong-log-transformer: ^2.1.0 - checksum: 6be434a3d8aaf41e290dd0169133417cdb3b33ffd59fe77c7a927f28302fb8712a0be63fd261cf1b9c601000ed4dba1f86f8c0a8c3fa97fc665cd4e3458fc1ba + checksum: f481252bd3aa2b1dc61fedf527840e5acf19e893ad15e3589ffa2466110d11f595b86402197cbc03fe8286339d8da1076f3bb25ef3d618a7aa4d18417a63e7e7 languageName: node linkType: hard -"@lerna/create@npm:7.4.1": - version: 7.4.1 - resolution: "@lerna/create@npm:7.4.1" +"@lerna/clean@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/clean@npm:5.5.4" dependencies: - "@lerna/child-process": 7.4.1 - "@npmcli/run-script": 6.0.2 - "@nx/devkit": ">=16.5.1 < 17" - "@octokit/plugin-enterprise-rest": 6.0.1 - "@octokit/rest": 19.0.11 - byte-size: 8.1.1 - chalk: 4.1.0 - clone-deep: 4.0.1 - cmd-shim: 6.0.1 - columnify: 1.6.0 - conventional-changelog-core: 5.0.1 - conventional-recommended-bump: 7.0.1 - cosmiconfig: ^8.2.0 - dedent: 0.7.0 - execa: 5.0.0 - fs-extra: ^11.1.1 - get-stream: 6.0.0 - git-url-parse: 13.1.0 - glob-parent: 5.1.2 - globby: 11.1.0 - graceful-fs: 4.2.11 - has-unicode: 2.0.1 - ini: ^1.3.8 - init-package-json: 5.0.0 - inquirer: ^8.2.4 - is-ci: 3.0.1 - is-stream: 2.0.0 - js-yaml: 4.1.0 - libnpmpublish: 7.3.0 - load-json-file: 6.2.0 - lodash: ^4.17.21 - make-dir: 4.0.0 - minimatch: 3.0.5 - multimatch: 5.0.0 - node-fetch: 2.6.7 - npm-package-arg: 8.1.1 - npm-packlist: 5.1.1 - npm-registry-fetch: ^14.0.5 + "@lerna/command": 5.5.4 + "@lerna/filter-options": 5.5.4 + "@lerna/prompt": 5.5.4 + "@lerna/pulse-till-done": 5.5.4 + "@lerna/rimraf-dir": 5.5.4 + p-map: ^4.0.0 + p-map-series: ^2.1.0 + p-waterfall: ^2.1.1 + checksum: cf2aadf90f825cf5d458ba4dd4e4182e40983b23b6b3cd6ffc7ff9780b02f7552ca4106007e2af080728470a6e935ec1ba0a925b21d806fce1eb290838e0ee06 + languageName: node + linkType: hard + +"@lerna/cli@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/cli@npm:5.5.4" + dependencies: + "@lerna/global-options": 5.5.4 + dedent: ^0.7.0 + npmlog: ^6.0.2 + yargs: ^16.2.0 + checksum: 54f4106233550c98fabd3771d4f813f2e0284a6d8e71f8fd7105fcade317cc46016529441af0042dd38c713a35d4f6c992fa0add51025667b729c674f630a2da + languageName: node + linkType: hard + +"@lerna/collect-uncommitted@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/collect-uncommitted@npm:5.5.4" + dependencies: + "@lerna/child-process": 5.5.4 + chalk: ^4.1.0 + npmlog: ^6.0.2 + checksum: 3d0c1a9526651499799df689974f9e8efb4a3aac760f421ae18013cadd53c62eda4d1d3dbd152ceb8c864096b71ade01aeab5335e461495a12159a7cb33119d9 + languageName: node + linkType: hard + +"@lerna/collect-updates@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/collect-updates@npm:5.5.4" + dependencies: + "@lerna/child-process": 5.5.4 + "@lerna/describe-ref": 5.5.4 + minimatch: ^3.0.4 npmlog: ^6.0.2 - nx: ">=16.5.1 < 17" - p-map: 4.0.0 - p-map-series: 2.1.0 - p-queue: 6.6.2 - p-reduce: ^2.1.0 - pacote: ^15.2.0 - pify: 5.0.0 - read-cmd-shim: 4.0.0 - read-package-json: 6.0.4 - resolve-from: 5.0.0 - rimraf: ^4.4.1 - semver: ^7.3.4 - signal-exit: 3.0.7 slash: ^3.0.0 - ssri: ^9.0.1 - strong-log-transformer: 2.1.0 - tar: 6.1.11 - temp-dir: 1.0.0 - upath: 2.0.1 - uuid: ^9.0.0 + checksum: dc051fdd205099dd005520549e50bf0c94a318c7d0e6ab51246e2278525c66dc98f513d3bbde7b33a9e9dd9d6d6d811666527570a56c0c9cadedca32db156969 + languageName: node + linkType: hard + +"@lerna/command@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/command@npm:5.5.4" + dependencies: + "@lerna/child-process": 5.5.4 + "@lerna/package-graph": 5.5.4 + "@lerna/project": 5.5.4 + "@lerna/validation-error": 5.5.4 + "@lerna/write-log-file": 5.5.4 + clone-deep: ^4.0.1 + dedent: ^0.7.0 + execa: ^5.0.0 + is-ci: ^2.0.0 + npmlog: ^6.0.2 + checksum: 096aadc9e3c0c0dc9f6127af9f655058360658ca73ffea476998a89594e29c6492894311ee7021df7532273b657c930d1060917b488ffccdff7643fa65bbeb25 + languageName: node + linkType: hard + +"@lerna/conventional-commits@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/conventional-commits@npm:5.5.4" + dependencies: + "@lerna/validation-error": 5.5.4 + conventional-changelog-angular: ^5.0.12 + conventional-changelog-core: ^4.2.4 + conventional-recommended-bump: ^6.1.0 + fs-extra: ^9.1.0 + get-stream: ^6.0.0 + npm-package-arg: 8.1.1 + npmlog: ^6.0.2 + pify: ^5.0.0 + semver: ^7.3.4 + checksum: 866731a1e1ff2bcb9795a10f6b462935f3e98bf353f24b7f04deea31c58a128e99ca9aabab24a6eb2181ea5f3f2e645d437bb485750cd2876853d48ec78211c0 + languageName: node + linkType: hard + +"@lerna/create-symlink@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/create-symlink@npm:5.5.4" + dependencies: + cmd-shim: ^5.0.0 + fs-extra: ^9.1.0 + npmlog: ^6.0.2 + checksum: 05c1bc24f450fc74b38991fd6a7f8a6df83b6777fe9456e1a0875071b032289942cba9d43173caef19df8946ef9eac7d423ed8c0fcb78980ddc6f24884298990 + languageName: node + linkType: hard + +"@lerna/create@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/create@npm:5.5.4" + dependencies: + "@lerna/child-process": 5.5.4 + "@lerna/command": 5.5.4 + "@lerna/npm-conf": 5.5.4 + "@lerna/validation-error": 5.5.4 + dedent: ^0.7.0 + fs-extra: ^9.1.0 + globby: ^11.0.2 + init-package-json: ^3.0.2 + npm-package-arg: 8.1.1 + p-reduce: ^2.1.0 + pacote: ^13.6.1 + pify: ^5.0.0 + semver: ^7.3.4 + slash: ^3.0.0 validate-npm-package-license: ^3.0.4 - validate-npm-package-name: 5.0.0 - write-file-atomic: 5.0.1 - write-pkg: 4.0.0 - yargs: 16.2.0 + validate-npm-package-name: ^4.0.0 yargs-parser: 20.2.4 - checksum: c0762cf8bb127a6c59c714dc0e0382f1d8f65f3f68ac968bf85798f2c32e0483d589a1a6ccfc03f98d036e8b3841e842159bcb81c5b4212464c2a328745e325e + checksum: 44e63b3ea4cae77abd0fbd1fd5227e6d5691a67f48e8bcf53bb657a6014230ed3baf6584982a1c25811c47d751563e59850224dd3291c61ca30a7fb8ef69eff7 + languageName: node + linkType: hard + +"@lerna/describe-ref@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/describe-ref@npm:5.5.4" + dependencies: + "@lerna/child-process": 5.5.4 + npmlog: ^6.0.2 + checksum: 2ba2d0a8e6f6d81b007a42b1c799f5759b68003ca93a922b091450cd66cd08ae60e5c55e306883609f7762a253a07215f40aa03c5c0652348acab9ede4d09848 + languageName: node + linkType: hard + +"@lerna/diff@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/diff@npm:5.5.4" + dependencies: + "@lerna/child-process": 5.5.4 + "@lerna/command": 5.5.4 + "@lerna/validation-error": 5.5.4 + npmlog: ^6.0.2 + checksum: 5a171e653c3074bc1c1d4d200dd2a82cf36e92ad7f9ab03f9d38cd9cd33b44ea0a568c3804d5d15323931ee353f51870ba3f000a917c041df77a8412bcffdb2f + languageName: node + linkType: hard + +"@lerna/exec@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/exec@npm:5.5.4" + dependencies: + "@lerna/child-process": 5.5.4 + "@lerna/command": 5.5.4 + "@lerna/filter-options": 5.5.4 + "@lerna/profiler": 5.5.4 + "@lerna/run-topologically": 5.5.4 + "@lerna/validation-error": 5.5.4 + p-map: ^4.0.0 + checksum: 90ba92303def5a1d39e15c3275282e6782005d52c7c880d2d1305f87c6f4a641cff67e2d1ef86dea46c8d18a1a03cb4b21ead7c60306cde5e9fb1d00396086fa + languageName: node + linkType: hard + +"@lerna/filter-options@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/filter-options@npm:5.5.4" + dependencies: + "@lerna/collect-updates": 5.5.4 + "@lerna/filter-packages": 5.5.4 + dedent: ^0.7.0 + npmlog: ^6.0.2 + checksum: a3fc09f042a66373231237fd9521b2d6e35ea56cb2cdf428de80fe7817cf27130718db2ddf68cfd7d7269c34f1cf266208c8e5c8b8dc2fa469fe8c35cbea8ee4 + languageName: node + linkType: hard + +"@lerna/filter-packages@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/filter-packages@npm:5.5.4" + dependencies: + "@lerna/validation-error": 5.5.4 + multimatch: ^5.0.0 + npmlog: ^6.0.2 + checksum: 889c26a659228c041f70ce27c81feaa360e8659299851208c931b726983449a885e943ca50db7c975b670adb07424c83bba0d0f4dc71ea99f9f6143b88a05315 + languageName: node + linkType: hard + +"@lerna/get-npm-exec-opts@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/get-npm-exec-opts@npm:5.5.4" + dependencies: + npmlog: ^6.0.2 + checksum: b4c0e88a53a32eb538b0730b316d7df51281d11d05f2ed928b6ef553c25611af1346921856999b6a8d69ff64742f51dde3e32156dba37ff950206bdebf51048f + languageName: node + linkType: hard + +"@lerna/get-packed@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/get-packed@npm:5.5.4" + dependencies: + fs-extra: ^9.1.0 + ssri: ^9.0.1 + tar: ^6.1.0 + checksum: e53c57740651086c0d5c3eb8aa5a9cd777e7aa9b4ec8bb3d997f1d9df783f67597c6418d73c5d680926998f45fb3ff3735fb72e232098bfb6e788c5fee416a5f + languageName: node + linkType: hard + +"@lerna/github-client@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/github-client@npm:5.5.4" + dependencies: + "@lerna/child-process": 5.5.4 + "@octokit/plugin-enterprise-rest": ^6.0.1 + "@octokit/rest": ^19.0.3 + git-url-parse: ^13.1.0 + npmlog: ^6.0.2 + checksum: 6f531f1c133c2643fa7c716c0d986bf59d18eed13099195fcf4c7fe683b49dc7fad1d410985d3f5273ec0f607763ef800b4d0d841557adc4a3fe4fe05a78dca2 + languageName: node + linkType: hard + +"@lerna/gitlab-client@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/gitlab-client@npm:5.5.4" + dependencies: + node-fetch: ^2.6.1 + npmlog: ^6.0.2 + checksum: bcd867c6e66f5eaa790c2c40a85f88b7fc37dc2504b17d096102630049bcb3dbf6d0ec33e8369fb6204e5537a263bbaeefacae42ab8570e104a5f5a17fa59685 + languageName: node + linkType: hard + +"@lerna/global-options@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/global-options@npm:5.5.4" + checksum: 5a501be802d3bc02f8525a8ee32bab7833e387323b7b52e86c8ed273ed197ca81aa12651a462a98eef11cabd61dca1e5e1a2390023cb056dae30da246ca94b72 + languageName: node + linkType: hard + +"@lerna/has-npm-version@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/has-npm-version@npm:5.5.4" + dependencies: + "@lerna/child-process": 5.5.4 + semver: ^7.3.4 + checksum: e28690f9efc7034da6f0e49b84816a184cea95376088c526621395add30a4e3475f5b66fc4945e126412e72aaa67cf557af236a65d5905084485f0acaf129140 + languageName: node + linkType: hard + +"@lerna/import@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/import@npm:5.5.4" + dependencies: + "@lerna/child-process": 5.5.4 + "@lerna/command": 5.5.4 + "@lerna/prompt": 5.5.4 + "@lerna/pulse-till-done": 5.5.4 + "@lerna/validation-error": 5.5.4 + dedent: ^0.7.0 + fs-extra: ^9.1.0 + p-map-series: ^2.1.0 + checksum: e04b2e85fef25c1ced9f98e607cf6ad25f3e98fe8fe80ae359614c34de4679c45b082a8cae164c6fbe6af86d9ce72128640a225bd603d3faa89d285406adf1f9 + languageName: node + linkType: hard + +"@lerna/info@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/info@npm:5.5.4" + dependencies: + "@lerna/command": 5.5.4 + "@lerna/output": 5.5.4 + envinfo: ^7.7.4 + checksum: 2bfb409a6b60bf2e7f755fe8443adbd3a414a50ea99119294c949770fa800ab6ebbc0a05c6646f2aed1d503ec1b7bfbf06688cfa7ae090d1132b4f041456ac6e + languageName: node + linkType: hard + +"@lerna/init@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/init@npm:5.5.4" + dependencies: + "@lerna/child-process": 5.5.4 + "@lerna/command": 5.5.4 + "@lerna/project": 5.5.4 + fs-extra: ^9.1.0 + p-map: ^4.0.0 + write-json-file: ^4.3.0 + checksum: 610bfc08593d54095898e02adc1e49b742eb385cf6168ec0c134e2f04231fa695924f5bfd404730fc11ae72dfb8700f408fb6514d56333e6e4cc46d4547563a1 + languageName: node + linkType: hard + +"@lerna/link@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/link@npm:5.5.4" + dependencies: + "@lerna/command": 5.5.4 + "@lerna/package-graph": 5.5.4 + "@lerna/symlink-dependencies": 5.5.4 + "@lerna/validation-error": 5.5.4 + p-map: ^4.0.0 + slash: ^3.0.0 + checksum: 391cb0e93f324cb1b7e72c7a415f23ab0690912ff72f0e40b5794ad7c32c4ecd3500f75efac73efd08dc83978aa23bf343b527d2b501d156e8f857ee9fe4f1d9 + languageName: node + linkType: hard + +"@lerna/list@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/list@npm:5.5.4" + dependencies: + "@lerna/command": 5.5.4 + "@lerna/filter-options": 5.5.4 + "@lerna/listable": 5.5.4 + "@lerna/output": 5.5.4 + checksum: ccbea2a102b6c9ebfdfb38bd8fac81a329a8c8bcd466e334ebb7626bf094361fff00a5895523744f12ba3ea4ed4e9798f8ddebc8006fb642f758b79014a3b64e + languageName: node + linkType: hard + +"@lerna/listable@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/listable@npm:5.5.4" + dependencies: + "@lerna/query-graph": 5.5.4 + chalk: ^4.1.0 + columnify: ^1.6.0 + checksum: db4e674fc75f320e5888a2ada8c2447d5111584f3c35e744a6d8fb9a026abe557ad6d02430915aeb7b4cb99cd129f391825053fa785d4ce2db2df462291c8ded + languageName: node + linkType: hard + +"@lerna/log-packed@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/log-packed@npm:5.5.4" + dependencies: + byte-size: ^7.0.0 + columnify: ^1.6.0 + has-unicode: ^2.0.1 + npmlog: ^6.0.2 + checksum: 83af3e1b63e658b9fde75feb9508a38492f23743ce028d74bde674ffba01bb7d9b0607f751f42c3b805c6b1a5c2ca70cd4e7ec70997f200875ce5ff4e4798e51 + languageName: node + linkType: hard + +"@lerna/npm-conf@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/npm-conf@npm:5.5.4" + dependencies: + config-chain: ^1.1.12 + pify: ^5.0.0 + checksum: dfbad876fd5bb92d6a34f0f4ec58eeaeaea323cd40be0e7b7cc8235093af7c855be3fd1aa689992aa884e404f5a16d38ea7a4c244f112b43f630d18aef8a162d + languageName: node + linkType: hard + +"@lerna/npm-dist-tag@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/npm-dist-tag@npm:5.5.4" + dependencies: + "@lerna/otplease": 5.5.4 + npm-package-arg: 8.1.1 + npm-registry-fetch: ^13.3.0 + npmlog: ^6.0.2 + checksum: 4fb1fa54c1dd2e6a5c5ab68723a753bc74542f7031e513c5ada7507496f3dae10884a6727486024b1b6f272c81d4e1a1f63dfc012180d3e27b896689dc79f402 + languageName: node + linkType: hard + +"@lerna/npm-install@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/npm-install@npm:5.5.4" + dependencies: + "@lerna/child-process": 5.5.4 + "@lerna/get-npm-exec-opts": 5.5.4 + fs-extra: ^9.1.0 + npm-package-arg: 8.1.1 + npmlog: ^6.0.2 + signal-exit: ^3.0.3 + write-pkg: ^4.0.0 + checksum: 156524225ab1e504e86aa025c464b1557f04e0cc72ff1c288afe69334eea3b394640ee0e48582e9cae357daa186a14066a8ad6be12eb4f497ac1cbe5bc3f1df5 + languageName: node + linkType: hard + +"@lerna/npm-publish@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/npm-publish@npm:5.5.4" + dependencies: + "@lerna/otplease": 5.5.4 + "@lerna/run-lifecycle": 5.5.4 + fs-extra: ^9.1.0 + libnpmpublish: ^6.0.4 + npm-package-arg: 8.1.1 + npmlog: ^6.0.2 + pify: ^5.0.0 + read-package-json: ^5.0.1 + checksum: 6a28621b59bc91a411c78f87d2b6aa9bdfd406cd7b2e05b448c5e94fdcab4643933cbc6b708e013ad158f9b839bb0ac8d305b92a2bd398ebc3e7fed72407af27 + languageName: node + linkType: hard + +"@lerna/npm-run-script@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/npm-run-script@npm:5.5.4" + dependencies: + "@lerna/child-process": 5.5.4 + "@lerna/get-npm-exec-opts": 5.5.4 + npmlog: ^6.0.2 + checksum: 488d32847ac2f15e7d8c5ef3564438029fecc92da5b643f5b642bb0706e041ca93c753b36f0325a4c62e78226379de201a1eb5b65c32287421dee3d0fb0d84c0 + languageName: node + linkType: hard + +"@lerna/otplease@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/otplease@npm:5.5.4" + dependencies: + "@lerna/prompt": 5.5.4 + checksum: 13970bae350dbbc58588a475ee706b1c5ecff556dfbc9858da0d0fc75f5687d608e0bd134260c354fbbc0108d9bdac6b2f2cc3e1c61f5cba9c188210835eae7e + languageName: node + linkType: hard + +"@lerna/output@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/output@npm:5.5.4" + dependencies: + npmlog: ^6.0.2 + checksum: f72633c06f8052c8283d039e10fa7aa4e9c965d6c838bdc736bd51d635a0ba20e173fc2e19173f3537b6d94a5881ac964b10ebaf99704ec5105a303563b64afe + languageName: node + linkType: hard + +"@lerna/pack-directory@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/pack-directory@npm:5.5.4" + dependencies: + "@lerna/get-packed": 5.5.4 + "@lerna/package": 5.5.4 + "@lerna/run-lifecycle": 5.5.4 + "@lerna/temp-write": 5.5.4 + npm-packlist: ^5.1.1 + npmlog: ^6.0.2 + tar: ^6.1.0 + checksum: 1d31a76e463957e8441e53d96b228da92daa1c2c242a9a1d2b4a4e95fca710f3b6f9e01f2f129cb33f0a9bf3d3e63ae579cf8f79f48c774ba604078b90b8775b + languageName: node + linkType: hard + +"@lerna/package-graph@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/package-graph@npm:5.5.4" + dependencies: + "@lerna/prerelease-id-from-version": 5.5.4 + "@lerna/validation-error": 5.5.4 + npm-package-arg: 8.1.1 + npmlog: ^6.0.2 + semver: ^7.3.4 + checksum: 4e48d8993eec4e381f817535e0f45472191889ba596e812941caeaf7fd57b28c7cd24e27ad23085db5ad1df6a82498812521b9698e2fad5d8be23d2d7a376f9d + languageName: node + linkType: hard + +"@lerna/package@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/package@npm:5.5.4" + dependencies: + load-json-file: ^6.2.0 + npm-package-arg: 8.1.1 + write-pkg: ^4.0.0 + checksum: 9e2ef5f6c43f02f8f81ccc33b6a195f5a3b505e0112a8a72c8169211249abeff1ccc1480a6f9dcf7e23068541cefa0eb26541fabf867d452657145aaef88dd6b + languageName: node + linkType: hard + +"@lerna/prerelease-id-from-version@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/prerelease-id-from-version@npm:5.5.4" + dependencies: + semver: ^7.3.4 + checksum: 6213fe4dc060d7c41e153e4e6c1f6214bad88e911659846ea39ccc874801d276c7751b836cf0bf17cb63e45943f1c67396717f6843fe58c6a31806a49c26cbd7 + languageName: node + linkType: hard + +"@lerna/profiler@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/profiler@npm:5.5.4" + dependencies: + fs-extra: ^9.1.0 + npmlog: ^6.0.2 + upath: ^2.0.1 + checksum: 1c3eb01eccf7d478ee3197886b60d43f743058c9f2591bf40d0b0737d263af3169a9d55d42fcc7b02c41d60cba1ac4bee1f146318c4c2e85a9892b0190b2d3ae + languageName: node + linkType: hard + +"@lerna/project@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/project@npm:5.5.4" + dependencies: + "@lerna/package": 5.5.4 + "@lerna/validation-error": 5.5.4 + cosmiconfig: ^7.0.0 + dedent: ^0.7.0 + dot-prop: ^6.0.1 + glob-parent: ^5.1.1 + globby: ^11.0.2 + js-yaml: ^4.1.0 + load-json-file: ^6.2.0 + npmlog: ^6.0.2 + p-map: ^4.0.0 + resolve-from: ^5.0.0 + write-json-file: ^4.3.0 + checksum: a97b76a6fc655e7d3177f9bfeff4da5e2b353322dfbeb70ae619fdd2b69f0ab6b2d2cb772388f396f8b21fbe0fade882303fbed9fb5c476a26f9b3b7aaa722dc + languageName: node + linkType: hard + +"@lerna/prompt@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/prompt@npm:5.5.4" + dependencies: + inquirer: ^8.2.4 + npmlog: ^6.0.2 + checksum: 652293aac0a159bc4eea11c85014e8368447f36b18810b7c92f3c33614890e74528d77ddceccd7ea038b3aa7f3976428329b34ca6f7cad8b424502bf64240b40 + languageName: node + linkType: hard + +"@lerna/publish@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/publish@npm:5.5.4" + dependencies: + "@lerna/check-working-tree": 5.5.4 + "@lerna/child-process": 5.5.4 + "@lerna/collect-updates": 5.5.4 + "@lerna/command": 5.5.4 + "@lerna/describe-ref": 5.5.4 + "@lerna/log-packed": 5.5.4 + "@lerna/npm-conf": 5.5.4 + "@lerna/npm-dist-tag": 5.5.4 + "@lerna/npm-publish": 5.5.4 + "@lerna/otplease": 5.5.4 + "@lerna/output": 5.5.4 + "@lerna/pack-directory": 5.5.4 + "@lerna/prerelease-id-from-version": 5.5.4 + "@lerna/prompt": 5.5.4 + "@lerna/pulse-till-done": 5.5.4 + "@lerna/run-lifecycle": 5.5.4 + "@lerna/run-topologically": 5.5.4 + "@lerna/validation-error": 5.5.4 + "@lerna/version": 5.5.4 + fs-extra: ^9.1.0 + libnpmaccess: ^6.0.3 + npm-package-arg: 8.1.1 + npm-registry-fetch: ^13.3.0 + npmlog: ^6.0.2 + p-map: ^4.0.0 + p-pipe: ^3.1.0 + pacote: ^13.6.1 + semver: ^7.3.4 + checksum: 466de9cade594c1f9bcb28f6e68dd51b7180a2eda864b0a55b46ddca59250ed7b91c996bd2f8a10ce6024e8f9b914561832b07cf149c8e7c66d596a3159591cc + languageName: node + linkType: hard + +"@lerna/pulse-till-done@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/pulse-till-done@npm:5.5.4" + dependencies: + npmlog: ^6.0.2 + checksum: a296b1617590188ad51da84020afc09db906a98c2d9b993bb93db493a8d9297dfdecdedc31e7f52c996e3b9fec9891c4d8dea7f5a7da912b8e711aa320ab6901 + languageName: node + linkType: hard + +"@lerna/query-graph@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/query-graph@npm:5.5.4" + dependencies: + "@lerna/package-graph": 5.5.4 + checksum: b360f980ff5ab5706a61b11b5d3ea256a729142b52af5c41a926e481b9722c8a04ca9d0c9a4a474a6258c49365ae8fdc3188be37a97acd48cec2174a09a9bb52 + languageName: node + linkType: hard + +"@lerna/resolve-symlink@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/resolve-symlink@npm:5.5.4" + dependencies: + fs-extra: ^9.1.0 + npmlog: ^6.0.2 + read-cmd-shim: ^3.0.0 + checksum: b3ddc7c92404385d6ba8a67cd63594192f84655af19cbe5fcc781f4d0d09348c480bf6d8eb86b689a02dac0e729fbf97355f3a6645312098992d2a47d1db57a0 + languageName: node + linkType: hard + +"@lerna/rimraf-dir@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/rimraf-dir@npm:5.5.4" + dependencies: + "@lerna/child-process": 5.5.4 + npmlog: ^6.0.2 + path-exists: ^4.0.0 + rimraf: ^3.0.2 + checksum: fd7255b7fcd895db588eb01b2c4387d9f43b0b618b1dfba426250545e2c3e3586bdaf8274ea4631c5f574cb973e6daf2c3fda1aee69dfa75d4ee5b681d8689dc + languageName: node + linkType: hard + +"@lerna/run-lifecycle@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/run-lifecycle@npm:5.5.4" + dependencies: + "@lerna/npm-conf": 5.5.4 + "@npmcli/run-script": ^4.1.7 + npmlog: ^6.0.2 + p-queue: ^6.6.2 + checksum: 3bbf90da32e83f3a909b190a174c215aa14ebc90c2eba62de79216d6c3c08411abba03d3dee1f2146c5552a36b9507ec0550ce38769265a5ede5f1f8c0b1fd75 + languageName: node + linkType: hard + +"@lerna/run-topologically@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/run-topologically@npm:5.5.4" + dependencies: + "@lerna/query-graph": 5.5.4 + p-queue: ^6.6.2 + checksum: 2fc3f2bcc6180e6b41e8cbc5bda35180c1fc6f69e6cf17306bc532c8d59667104dc3f65e6f05669b9cd222e06788ea4d110503bd26f75204657edb01d7389ae0 + languageName: node + linkType: hard + +"@lerna/run@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/run@npm:5.5.4" + dependencies: + "@lerna/command": 5.5.4 + "@lerna/filter-options": 5.5.4 + "@lerna/npm-run-script": 5.5.4 + "@lerna/output": 5.5.4 + "@lerna/profiler": 5.5.4 + "@lerna/run-topologically": 5.5.4 + "@lerna/timer": 5.5.4 + "@lerna/validation-error": 5.5.4 + fs-extra: ^9.1.0 + p-map: ^4.0.0 + checksum: aab32307bf40ff5c6bd061deaa1911068f1f02125b599148e1b7b8344ce26209ba7807f86a713531710fe7ffb46186980ffaf801b3f3151afb8e4952b5ab6ef6 + languageName: node + linkType: hard + +"@lerna/symlink-binary@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/symlink-binary@npm:5.5.4" + dependencies: + "@lerna/create-symlink": 5.5.4 + "@lerna/package": 5.5.4 + fs-extra: ^9.1.0 + p-map: ^4.0.0 + checksum: a4bff1050a379f237fbcfe7145ea1b3ad0ccce3daff44be19dd3ca96f542d78cec24be7ac14ef97163fb4024c679ee1d748c8035faf4cedc3a0270fc965b9f4d + languageName: node + linkType: hard + +"@lerna/symlink-dependencies@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/symlink-dependencies@npm:5.5.4" + dependencies: + "@lerna/create-symlink": 5.5.4 + "@lerna/resolve-symlink": 5.5.4 + "@lerna/symlink-binary": 5.5.4 + fs-extra: ^9.1.0 + p-map: ^4.0.0 + p-map-series: ^2.1.0 + checksum: d7675966af7cb83a8e0a963e4b08049be4cdf4381196d4fbc975f571a5b3f5f0bf5002b5c15a5256ae861bacf86856469fa2bc90f64270998f4ac529bbdb08ed + languageName: node + linkType: hard + +"@lerna/temp-write@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/temp-write@npm:5.5.4" + dependencies: + graceful-fs: ^4.1.15 + is-stream: ^2.0.0 + make-dir: ^3.0.0 + temp-dir: ^1.0.0 + uuid: ^8.3.2 + checksum: f9d61e997dd10d4445da1f85582c4649b8ee1bcf3e9cd3ce52013cc88d3f39b7b26445dfe7a222eb5fde2687d759281a5222994fe1dcf1fa5f3d68dec22a51a6 + languageName: node + linkType: hard + +"@lerna/timer@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/timer@npm:5.5.4" + checksum: b15c10881a5e2e8e6c42643bdb1d1e0c73930545f522f161fb24c007bf373b8d7d59694cd24d1ea17bc631f72bd40f45095a538bd86b03fa7161febec4ec94c5 + languageName: node + linkType: hard + +"@lerna/validation-error@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/validation-error@npm:5.5.4" + dependencies: + npmlog: ^6.0.2 + checksum: 86cc66dd8f3d35ff333ad6e5009f176cb21c3e073e4591dc714f18d7f8d44cd3838ae365d5e9d927d7d8a08248296b76048f7d5948052bddecc9920ad8f5a013 + languageName: node + linkType: hard + +"@lerna/version@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/version@npm:5.5.4" + dependencies: + "@lerna/check-working-tree": 5.5.4 + "@lerna/child-process": 5.5.4 + "@lerna/collect-updates": 5.5.4 + "@lerna/command": 5.5.4 + "@lerna/conventional-commits": 5.5.4 + "@lerna/github-client": 5.5.4 + "@lerna/gitlab-client": 5.5.4 + "@lerna/output": 5.5.4 + "@lerna/prerelease-id-from-version": 5.5.4 + "@lerna/prompt": 5.5.4 + "@lerna/run-lifecycle": 5.5.4 + "@lerna/run-topologically": 5.5.4 + "@lerna/temp-write": 5.5.4 + "@lerna/validation-error": 5.5.4 + chalk: ^4.1.0 + dedent: ^0.7.0 + load-json-file: ^6.2.0 + minimatch: ^3.0.4 + npmlog: ^6.0.2 + p-map: ^4.0.0 + p-pipe: ^3.1.0 + p-reduce: ^2.1.0 + p-waterfall: ^2.1.1 + semver: ^7.3.4 + slash: ^3.0.0 + write-json-file: ^4.3.0 + checksum: 785d1cfb837cd6c2559a4f777ee621fd760019d591b6e878b2845d257595bdad4eb3a11710beac91e1a2244b95f7ca708b576ecf59a8747cc85de074b7f0e086 + languageName: node + linkType: hard + +"@lerna/write-log-file@npm:5.5.4": + version: 5.5.4 + resolution: "@lerna/write-log-file@npm:5.5.4" + dependencies: + npmlog: ^6.0.2 + write-file-atomic: ^4.0.1 + checksum: c25c235f5399e0d510a4b6d7b3a675d0a8dabd8b84dd663fbf713a4174e234e21bc1b5d35c50f442cc4e57000482955bbd351fbd9181cd56fe4f5dd730ada001 languageName: node linkType: hard @@ -4331,6 +5058,50 @@ __metadata: languageName: node linkType: hard +"@npmcli/arborist@npm:5.3.0": + version: 5.3.0 + resolution: "@npmcli/arborist@npm:5.3.0" + dependencies: + "@isaacs/string-locale-compare": ^1.1.0 + "@npmcli/installed-package-contents": ^1.0.7 + "@npmcli/map-workspaces": ^2.0.3 + "@npmcli/metavuln-calculator": ^3.0.1 + "@npmcli/move-file": ^2.0.0 + "@npmcli/name-from-folder": ^1.0.1 + "@npmcli/node-gyp": ^2.0.0 + "@npmcli/package-json": ^2.0.0 + "@npmcli/run-script": ^4.1.3 + bin-links: ^3.0.0 + cacache: ^16.0.6 + common-ancestor-path: ^1.0.1 + json-parse-even-better-errors: ^2.3.1 + json-stringify-nice: ^1.1.4 + mkdirp: ^1.0.4 + mkdirp-infer-owner: ^2.0.0 + nopt: ^5.0.0 + npm-install-checks: ^5.0.0 + npm-package-arg: ^9.0.0 + npm-pick-manifest: ^7.0.0 + npm-registry-fetch: ^13.0.0 + npmlog: ^6.0.2 + pacote: ^13.6.1 + parse-conflict-json: ^2.0.1 + proc-log: ^2.0.0 + promise-all-reject-late: ^1.0.0 + promise-call-limit: ^1.0.1 + read-package-json-fast: ^2.0.2 + readdir-scoped-modules: ^1.1.0 + rimraf: ^3.0.2 + semver: ^7.3.7 + ssri: ^9.0.0 + treeverse: ^2.0.0 + walk-up-path: ^1.0.0 + bin: + arborist: bin/index.js + checksum: 7f99f451ba625dd3532e7a69b27cc399cab1e7ef2a069bbc04cf22ef9d16a0076f8f5fb92c4cd146c256cd8a41963b2e417684f063a108e96939c440bad0e95e + languageName: node + linkType: hard + "@npmcli/fs@npm:^1.0.0": version: 1.0.0 resolution: "@npmcli/fs@npm:1.0.0" @@ -4351,40 +5122,56 @@ __metadata: languageName: node linkType: hard -"@npmcli/fs@npm:^3.1.0": - version: 3.1.0 - resolution: "@npmcli/fs@npm:3.1.0" +"@npmcli/git@npm:^3.0.0": + version: 3.0.1 + resolution: "@npmcli/git@npm:3.0.1" dependencies: - semver: ^7.3.5 - checksum: a50a6818de5fc557d0b0e6f50ec780a7a02ab8ad07e5ac8b16bf519e0ad60a144ac64f97d05c443c3367235d337182e1d012bbac0eb8dbae8dc7b40b193efd0e - languageName: node - linkType: hard - -"@npmcli/git@npm:^4.0.0": - version: 4.1.0 - resolution: "@npmcli/git@npm:4.1.0" - dependencies: - "@npmcli/promise-spawn": ^6.0.0 + "@npmcli/promise-spawn": ^3.0.0 lru-cache: ^7.4.4 - npm-pick-manifest: ^8.0.0 - proc-log: ^3.0.0 + mkdirp: ^1.0.4 + npm-pick-manifest: ^7.0.0 + proc-log: ^2.0.0 promise-inflight: ^1.0.1 promise-retry: ^2.0.1 semver: ^7.3.5 - which: ^3.0.0 - checksum: 37efb926593f294eb263297cdfffec9141234f977b89a7a6b95ff7a72576c1d7f053f4961bc4b5e79dea6476fe08e0f3c1ed9e4aeb84169e357ff757a6a70073 + which: ^2.0.2 + checksum: 0e289d11e2d6034652993f2d05f68396d8377603a1c1f983b2d0893e7591a22bcf3896a43c7dfbcc43f03c308a110f0b9ec37e0191e48b0bd1d236e0f57a3ec6 languageName: node linkType: hard -"@npmcli/installed-package-contents@npm:^2.0.1": - version: 2.0.2 - resolution: "@npmcli/installed-package-contents@npm:2.0.2" +"@npmcli/installed-package-contents@npm:^1.0.7": + version: 1.0.7 + resolution: "@npmcli/installed-package-contents@npm:1.0.7" dependencies: - npm-bundled: ^3.0.0 - npm-normalize-package-bin: ^3.0.0 + npm-bundled: ^1.1.1 + npm-normalize-package-bin: ^1.0.1 bin: - installed-package-contents: lib/index.js - checksum: 60789d5ed209ee5df479232f62d9d38ecec36e95701cae88320b828b8651351b32d7b47d16d4c36cc7ce5000db4bf1f3e6981bed6381bdc5687ff4bc0795682d + installed-package-contents: index.js + checksum: a4a29b99d439827ce2e7817c1f61b56be160e640696e31dc513a2c8a37c792f75cdb6258ec15a1e22904f20df0a8a3019dd3766de5e6619f259834cf64233538 + languageName: node + linkType: hard + +"@npmcli/map-workspaces@npm:^2.0.3": + version: 2.0.3 + resolution: "@npmcli/map-workspaces@npm:2.0.3" + dependencies: + "@npmcli/name-from-folder": ^1.0.1 + glob: ^8.0.1 + minimatch: ^5.0.1 + read-package-json-fast: ^2.0.3 + checksum: c9878a22168d3f2d8df9e339ed0799628db3ea8502bd623b5bbe7b0dfcac065b3310e4093df94667a4a28ef2c54c02ce6956467a8aaa2e150305f2fe1cd64f9d + languageName: node + linkType: hard + +"@npmcli/metavuln-calculator@npm:^3.0.1": + version: 3.1.1 + resolution: "@npmcli/metavuln-calculator@npm:3.1.1" + dependencies: + cacache: ^16.0.0 + json-parse-even-better-errors: ^2.3.1 + pacote: ^13.0.3 + semver: ^7.3.5 + checksum: dc9846fdb82a1f4274ff8943f81452c75615bd9bca523c862956ea2c32e18c5a4be5572e169104d3a0eb262b7ede72c8dbbc202a4ab3b3f4946fa55f226dcc64 languageName: node linkType: hard @@ -4408,140 +5195,68 @@ __metadata: languageName: node linkType: hard -"@npmcli/node-gyp@npm:^3.0.0": +"@npmcli/name-from-folder@npm:^1.0.1": + version: 1.0.1 + resolution: "@npmcli/name-from-folder@npm:1.0.1" + checksum: 67339f4096e32b712d2df0250cc95c087569f09e657d7f81a1760fa2cc5123e29c3c3e1524388832310ba2d96ec4679985b643b44627f6a51f4a00c3b0075de9 + languageName: node + linkType: hard + +"@npmcli/node-gyp@npm:^2.0.0": + version: 2.0.0 + resolution: "@npmcli/node-gyp@npm:2.0.0" + checksum: b6bbf0015000f9b64d31aefdc30f244b0348c57adb64017667e0304e96c38644d83da46a4581252652f5d606268df49118f9c9993b41d8020f62b7b15dd2c8d8 + languageName: node + linkType: hard + +"@npmcli/package-json@npm:^2.0.0": + version: 2.0.0 + resolution: "@npmcli/package-json@npm:2.0.0" + dependencies: + json-parse-even-better-errors: ^2.3.1 + checksum: 7a598e42d2778654ec87438ebfafbcbafbe5a5f5e89ed2ca1db6ca3f94ef14655e304aa41f77632a2a3f5c66b6bd5960bd9370e0ceb4902ea09346720364f9e4 + languageName: node + linkType: hard + +"@npmcli/promise-spawn@npm:^3.0.0": version: 3.0.0 - resolution: "@npmcli/node-gyp@npm:3.0.0" - checksum: fe3802b813eecb4ade7ad77c9396cb56721664275faab027e3bd8a5e15adfbbe39e2ecc19f7885feb3cfa009b96632741cc81caf7850ba74440c6a2eee7b4ffc + resolution: "@npmcli/promise-spawn@npm:3.0.0" + dependencies: + infer-owner: ^1.0.4 + checksum: 3454465a2731cea5875ba51f80873e2205e5bd878c31517286b0ede4ea931c7bf3de895382287e906d03710fff6f9e44186bd0eee068ce578901c5d3b58e7692 languageName: node linkType: hard -"@npmcli/promise-spawn@npm:^6.0.0, @npmcli/promise-spawn@npm:^6.0.1": - version: 6.0.2 - resolution: "@npmcli/promise-spawn@npm:6.0.2" +"@npmcli/run-script@npm:^4.1.0, @npmcli/run-script@npm:^4.1.3, @npmcli/run-script@npm:^4.1.7": + version: 4.2.1 + resolution: "@npmcli/run-script@npm:4.2.1" dependencies: - which: ^3.0.0 - checksum: aa725780c13e1f97ab32ed7bcb5a207a3fb988e1d7ecdc3d22a549a22c8034740366b351c4dde4b011bcffcd8c4a7be6083d9cf7bc7e897b88837150de018528 - languageName: node - linkType: hard - -"@npmcli/run-script@npm:6.0.2, @npmcli/run-script@npm:^6.0.0": - version: 6.0.2 - resolution: "@npmcli/run-script@npm:6.0.2" - dependencies: - "@npmcli/node-gyp": ^3.0.0 - "@npmcli/promise-spawn": ^6.0.0 + "@npmcli/node-gyp": ^2.0.0 + "@npmcli/promise-spawn": ^3.0.0 node-gyp: ^9.0.0 - read-package-json-fast: ^3.0.0 - which: ^3.0.0 - checksum: 7a671d7dbeae376496e1c6242f02384928617dc66cd22881b2387272205c3668f8490ec2da4ad63e1abf979efdd2bdf4ea0926601d78578e07d83cfb233b3a1a + read-package-json-fast: ^2.0.3 + which: ^2.0.2 + checksum: 7b8d6676353f157e68b26baf848e01e5d887bcf90ce81a52f23fc9a5d93e6ffb60057532d664cfd7aeeb76d464d0c8b0d314ee6cccb56943acb3b6c570b756c8 languageName: node linkType: hard -"@nrwl/devkit@npm:16.10.0": - version: 16.10.0 - resolution: "@nrwl/devkit@npm:16.10.0" +"@nrwl/cli@npm:14.8.2": + version: 14.8.2 + resolution: "@nrwl/cli@npm:14.8.2" dependencies: - "@nx/devkit": 16.10.0 - checksum: 92c40138f7d107da82d14adca1cedb16ff45583f486cf624d047b2928521f92da6f69a5bdeae0bd98a37dfa553883843f36088ee6ace8d76a5170a5730b89a40 + nx: 14.8.2 + checksum: 18d698397cd0536109b1a6dbe50e9ec13063dde2793b49ab25d3db3f55ec74931ad20ae32375c5d2a1554d9c91f5b1152e42d6738aaca3ebecca4735bd4916c8 languageName: node linkType: hard -"@nrwl/tao@npm:16.10.0": - version: 16.10.0 - resolution: "@nrwl/tao@npm:16.10.0" +"@nrwl/tao@npm:14.8.2": + version: 14.8.2 + resolution: "@nrwl/tao@npm:14.8.2" dependencies: - nx: 16.10.0 - tslib: ^2.3.0 + nx: 14.8.2 bin: tao: index.js - checksum: a973a9fbed8fea33bfcb1b39b4bb29371ea00d116bbe7e39f2e7c8a9448b86e7c499d0aef79f262d9a993d103b4451d6749889e307212421b10838d49454a35c - languageName: node - linkType: hard - -"@nx/devkit@npm:16.10.0, @nx/devkit@npm:>=16.5.1 < 17": - version: 16.10.0 - resolution: "@nx/devkit@npm:16.10.0" - dependencies: - "@nrwl/devkit": 16.10.0 - ejs: ^3.1.7 - enquirer: ~2.3.6 - ignore: ^5.0.4 - semver: 7.5.3 - tmp: ~0.2.1 - tslib: ^2.3.0 - peerDependencies: - nx: ">= 15 <= 17" - checksum: f79f22be16d216aabc12df06f4f6d93026082c86114a99a66915f2993b4052ee8c66fd8eccad916e487a3f012890b89c4dd6a2ca8f3f95150ac824fab187a55a - languageName: node - linkType: hard - -"@nx/nx-darwin-arm64@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-darwin-arm64@npm:16.10.0" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@nx/nx-darwin-x64@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-darwin-x64@npm:16.10.0" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@nx/nx-freebsd-x64@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-freebsd-x64@npm:16.10.0" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"@nx/nx-linux-arm-gnueabihf@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-linux-arm-gnueabihf@npm:16.10.0" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"@nx/nx-linux-arm64-gnu@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-linux-arm64-gnu@npm:16.10.0" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - -"@nx/nx-linux-arm64-musl@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-linux-arm64-musl@npm:16.10.0" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - -"@nx/nx-linux-x64-gnu@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-linux-x64-gnu@npm:16.10.0" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"@nx/nx-linux-x64-musl@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-linux-x64-musl@npm:16.10.0" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - -"@nx/nx-win32-arm64-msvc@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-win32-arm64-msvc@npm:16.10.0" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@nx/nx-win32-x64-msvc@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-win32-x64-msvc@npm:16.10.0" - conditions: os=win32 & cpu=x64 + checksum: 78067a5c61b88c7cc43b0313dd1a96cc40149b84f349f2c634dd8ee5514b9d71deca28267a03fa081c8c5877d406e3774046c4927f0111c9f5c5571fd617e254 languageName: node linkType: hard @@ -4554,18 +5269,18 @@ __metadata: languageName: node linkType: hard -"@octokit/core@npm:^4.2.1": - version: 4.2.4 - resolution: "@octokit/core@npm:4.2.4" +"@octokit/core@npm:^4.0.0": + version: 4.0.4 + resolution: "@octokit/core@npm:4.0.4" dependencies: "@octokit/auth-token": ^3.0.0 "@octokit/graphql": ^5.0.0 "@octokit/request": ^6.0.0 "@octokit/request-error": ^3.0.0 - "@octokit/types": ^9.0.0 + "@octokit/types": ^6.0.3 before-after-hook: ^2.2.0 universal-user-agent: ^6.0.0 - checksum: ac8ab47440a31b0228a034aacac6994b64d6b073ad5b688b4c5157fc5ee0d1af1c926e6087bf17fd7244ee9c5998839da89065a90819bde4a97cb77d4edf58a6 + checksum: c9ae1e5706ab568a725cc5dba314049fbd37d77f1595dd2c19733abddfd72f4e1d46d6980e212d845dde4625ce5f170af951ac0eb0d7bc09e56a159b88cbe5dd languageName: node linkType: hard @@ -4598,29 +5313,21 @@ __metadata: languageName: node linkType: hard -"@octokit/openapi-types@npm:^18.0.0": - version: 18.1.1 - resolution: "@octokit/openapi-types@npm:18.1.1" - checksum: 94f42977fd2fcb9983c781fd199bc11218885a1226d492680bfb1268524a1b2af48a768eef90c63b80a2874437de641d59b3b7f640a5afa93e7c21fe1a79069a - languageName: node - linkType: hard - -"@octokit/plugin-enterprise-rest@npm:6.0.1": +"@octokit/plugin-enterprise-rest@npm:^6.0.1": version: 6.0.1 resolution: "@octokit/plugin-enterprise-rest@npm:6.0.1" checksum: 1c9720002f31daf62f4f48e73557dcdd7fcde6e0f6d43256e3f2ec827b5548417297186c361fb1af497fdcc93075a7b681e6ff06e2f20e4a8a3e74cc09d1f7e3 languageName: node linkType: hard -"@octokit/plugin-paginate-rest@npm:^6.1.2": - version: 6.1.2 - resolution: "@octokit/plugin-paginate-rest@npm:6.1.2" +"@octokit/plugin-paginate-rest@npm:^3.0.0": + version: 3.0.0 + resolution: "@octokit/plugin-paginate-rest@npm:3.0.0" dependencies: - "@octokit/tsconfig": ^1.0.2 - "@octokit/types": ^9.2.3 + "@octokit/types": ^6.39.0 peerDependencies: "@octokit/core": ">=4" - checksum: a7b3e686c7cbd27ec07871cde6e0b1dc96337afbcef426bbe3067152a17b535abd480db1861ca28c88d93db5f7bfdbcadd0919ead19818c28a69d0e194038065 + checksum: 1d2c900254f3dcd43f7ba69dfd12ff63f93a0d39a1bf542b1d0f006e95da4924ae0a26044c864ad7fb0309047f44becaf76293aae334d14c946910d65edd2523 languageName: node linkType: hard @@ -4633,14 +5340,15 @@ __metadata: languageName: node linkType: hard -"@octokit/plugin-rest-endpoint-methods@npm:^7.1.2": - version: 7.2.3 - resolution: "@octokit/plugin-rest-endpoint-methods@npm:7.2.3" +"@octokit/plugin-rest-endpoint-methods@npm:^6.0.0": + version: 6.1.2 + resolution: "@octokit/plugin-rest-endpoint-methods@npm:6.1.2" dependencies: - "@octokit/types": ^10.0.0 + "@octokit/types": ^6.40.0 + deprecation: ^2.3.1 peerDependencies: "@octokit/core": ">=3" - checksum: 21dfb98514dbe900c29cddb13b335bbce43d613800c6b17eba3c1fd31d17e69c1960f3067f7bf864bb38fdd5043391f4a23edee42729d8c7fbabd00569a80336 + checksum: 88ba028da00f73cf8a0471e9ffe0da2ba61c15b91fbaf252e33863bd4115db7dcd3bd426f22a01c7492affe89e37117fc3f0d15569cd5cf5a4b978f1bc22738b languageName: node linkType: hard @@ -4669,35 +5377,19 @@ __metadata: languageName: node linkType: hard -"@octokit/rest@npm:19.0.11": - version: 19.0.11 - resolution: "@octokit/rest@npm:19.0.11" +"@octokit/rest@npm:^19.0.3": + version: 19.0.3 + resolution: "@octokit/rest@npm:19.0.3" dependencies: - "@octokit/core": ^4.2.1 - "@octokit/plugin-paginate-rest": ^6.1.2 + "@octokit/core": ^4.0.0 + "@octokit/plugin-paginate-rest": ^3.0.0 "@octokit/plugin-request-log": ^1.0.4 - "@octokit/plugin-rest-endpoint-methods": ^7.1.2 - checksum: 147518ad51d214ead88adc717b5fdc4f33317949d58c124f4069bdf07d2e6b49fa66861036b9e233aed71fcb88ff367a6da0357653484e466175ab4fb7183b3b + "@octokit/plugin-rest-endpoint-methods": ^6.0.0 + checksum: 9ee96976c4c22dab11b3dacd541e694f3ad9bb1d44243985dc90ce6e8a42c3e3176a206e8d3a883b63b517fc15af8c8c88d8d0ecd9bac2b86a635a9667fc6ff4 languageName: node linkType: hard -"@octokit/tsconfig@npm:^1.0.2": - version: 1.0.2 - resolution: "@octokit/tsconfig@npm:1.0.2" - checksum: 74d56f3e9f326a8dd63700e9a51a7c75487180629c7a68bbafee97c612fbf57af8347369bfa6610b9268a3e8b833c19c1e4beb03f26db9a9dce31f6f7a19b5b1 - languageName: node - linkType: hard - -"@octokit/types@npm:^10.0.0": - version: 10.0.0 - resolution: "@octokit/types@npm:10.0.0" - dependencies: - "@octokit/openapi-types": ^18.0.0 - checksum: 8aafba2ff0cd2435fb70c291bf75ed071c0fa8a865cf6169648732068a35dec7b85a345851f18920ec5f3e94ee0e954988485caac0da09ec3f6781cc44fe153a - languageName: node - linkType: hard - -"@octokit/types@npm:^6.0.3, @octokit/types@npm:^6.16.1": +"@octokit/types@npm:^6.0.3, @octokit/types@npm:^6.16.1, @octokit/types@npm:^6.39.0, @octokit/types@npm:^6.40.0": version: 6.40.0 resolution: "@octokit/types@npm:6.40.0" dependencies: @@ -4706,15 +5398,6 @@ __metadata: languageName: node linkType: hard -"@octokit/types@npm:^9.0.0, @octokit/types@npm:^9.2.3": - version: 9.3.2 - resolution: "@octokit/types@npm:9.3.2" - dependencies: - "@octokit/openapi-types": ^18.0.0 - checksum: f55d096aaed3e04b8308d4422104fb888f355988056ba7b7ef0a4c397b8a3e54290d7827b06774dbe0c9ce55280b00db486286954f9c265aa6b03091026d9da8 - languageName: node - linkType: hard - "@open-draft/until@npm:^1.0.3": version: 1.0.3 resolution: "@open-draft/until@npm:1.0.3" @@ -6283,43 +6966,6 @@ __metadata: languageName: node linkType: hard -"@sigstore/bundle@npm:^1.1.0": - version: 1.1.0 - resolution: "@sigstore/bundle@npm:1.1.0" - dependencies: - "@sigstore/protobuf-specs": ^0.2.0 - checksum: 9bdd829f2867de6c03a19c5a7cff2c864887a9ed6e1c3438eb6659e838fde0b449fe83b1ca21efa00286a80c71e0144e20c0d9c415eead12e97d149285245c5a - languageName: node - linkType: hard - -"@sigstore/protobuf-specs@npm:^0.2.0": - version: 0.2.1 - resolution: "@sigstore/protobuf-specs@npm:0.2.1" - checksum: ddb7c829c7bf4148eccb571ede07cf9fda62f46b7b4d3a5ca02c0308c950ee90b4206b61082ee8d5753f24098632a8b24c147117bef8c68791bf5da537b55db9 - languageName: node - linkType: hard - -"@sigstore/sign@npm:^1.0.0": - version: 1.0.0 - resolution: "@sigstore/sign@npm:1.0.0" - dependencies: - "@sigstore/bundle": ^1.1.0 - "@sigstore/protobuf-specs": ^0.2.0 - make-fetch-happen: ^11.0.1 - checksum: cbdf409c39219d310f398e6a96b3ed7f422a58cfc0d8a40dd5b94996f805f189fdedf51afd559882bc18eb17054bf9d4f1a584b6af7b26c2f807636bceca5b19 - languageName: node - linkType: hard - -"@sigstore/tuf@npm:^1.0.3": - version: 1.0.3 - resolution: "@sigstore/tuf@npm:1.0.3" - dependencies: - "@sigstore/protobuf-specs": ^0.2.0 - tuf-js: ^1.1.7 - checksum: 0a32594b73ce3b3a4dfeec438ff98866a952a48ee6c020ddf57795062d9d328bc4327bb0e0c8d24011e3870c7d4670bc142a47025cbe7218c776f08084085421 - languageName: node - linkType: hard - "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -7844,23 +8490,6 @@ __metadata: languageName: node linkType: hard -"@tufjs/canonical-json@npm:1.0.0": - version: 1.0.0 - resolution: "@tufjs/canonical-json@npm:1.0.0" - checksum: 9ff3bcd12988fb23643690da3e009f9130b7b10974f8e7af4bd8ad230a228119de8609aa76d75264fe80f152b50872dea6ea53def69534436a4c24b4fcf6a447 - languageName: node - linkType: hard - -"@tufjs/models@npm:1.0.4": - version: 1.0.4 - resolution: "@tufjs/models@npm:1.0.4" - dependencies: - "@tufjs/canonical-json": 1.0.0 - minimatch: ^9.0.0 - checksum: b489baa854abce6865f360591c20d5eb7d8dde3fb150f42840c12bb7ee3e5e7a69eab9b2e44ea82ae1f8cd95b586963c5a5c5af8ba4ffa3614b3ddccbc306779 - languageName: node - linkType: hard - "@types/angular-route@npm:1.7.3": version: 1.7.3 resolution: "@types/angular-route@npm:1.7.3" @@ -10150,13 +10779,13 @@ __metadata: languageName: node linkType: hard -"@yarnpkg/parsers@npm:3.0.0-rc.46": - version: 3.0.0-rc.46 - resolution: "@yarnpkg/parsers@npm:3.0.0-rc.46" +"@yarnpkg/parsers@npm:^3.0.0-rc.18": + version: 3.0.0-rc.22 + resolution: "@yarnpkg/parsers@npm:3.0.0-rc.22" dependencies: js-yaml: ^3.10.0 tslib: ^2.4.0 - checksum: 35dfd1b1ac7ed9babf231721eb90b58156e840e575f6792a8e5ab559beaed6e2d60833b857310e67d6282c9406357648df2f510e670ec37ef4bd41657f329a51 + checksum: 4a31b4faad853b6cb09ff198017dd2f81782cb57ff8aaa2446ab9c8eb51aacaad3fa740e0c156c60c66cdb9cff8939f99b2b09c9890e2b8d015dcbed0150cb8a languageName: node linkType: hard @@ -10178,7 +10807,7 @@ __metadata: languageName: node linkType: hard -"JSONStream@npm:^1.3.5": +"JSONStream@npm:^1.0.4": version: 1.3.5 resolution: "JSONStream@npm:1.3.5" dependencies: @@ -10551,7 +11180,7 @@ __metadata: languageName: node linkType: hard -"aproba@npm:^1.0.3 || ^2.0.0": +"aproba@npm:^1.0.3 || ^2.0.0, aproba@npm:^2.0.0": version: 2.0.0 resolution: "aproba@npm:2.0.0" checksum: 5615cadcfb45289eea63f8afd064ab656006361020e1735112e346593856f87435e02d8dcc7ff0d11928bc7d425f27bc7c2a84f6c0b35ab0ff659c814c138a24 @@ -10786,6 +11415,13 @@ __metadata: languageName: node linkType: hard +"asap@npm:^2.0.0": + version: 2.0.6 + resolution: "asap@npm:2.0.6" + checksum: b296c92c4b969e973260e47523207cd5769abd27c245a68c26dc7a0fe8053c55bb04360237cb51cab1df52be939da77150ace99ad331fb7fb13b3423ed73ff3d + languageName: node + linkType: hard + "asap@npm:~1.0.0": version: 1.0.0 resolution: "asap@npm:1.0.0" @@ -10980,17 +11616,6 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.0.0": - version: 1.5.1 - resolution: "axios@npm:1.5.1" - dependencies: - follow-redirects: ^1.15.0 - form-data: ^4.0.0 - proxy-from-env: ^1.1.0 - checksum: 4444f06601f4ede154183767863d2b8e472b4a6bfc5253597ed6d21899887e1fd0ee2b3de792ac4f8459fe2e359d2aa07c216e45fd8b9e4e0688a6ebf48a5a8d - languageName: node - linkType: hard - "axobject-query@npm:^3.1.1": version: 3.1.1 resolution: "axobject-query@npm:3.1.1" @@ -11317,6 +11942,20 @@ __metadata: languageName: node linkType: hard +"bin-links@npm:^3.0.0": + version: 3.0.1 + resolution: "bin-links@npm:3.0.1" + dependencies: + cmd-shim: ^5.0.0 + mkdirp-infer-owner: ^2.0.0 + npm-normalize-package-bin: ^1.0.0 + read-cmd-shim: ^3.0.0 + rimraf: ^3.0.0 + write-file-atomic: ^4.0.0 + checksum: c608f0746c5851f259f7578ae5157d24fb019b00792d246bade6255136e5fbd41df43219a50d53f844c562afb6e41092a5f2b0be1bd890e08ff023d330327380 + languageName: node + linkType: hard + "binary-extensions@npm:^2.0.0": version: 2.2.0 resolution: "binary-extensions@npm:2.2.0" @@ -11615,10 +12254,10 @@ __metadata: languageName: node linkType: hard -"byte-size@npm:8.1.1": - version: 8.1.1 - resolution: "byte-size@npm:8.1.1" - checksum: 65f00881ffd3c2b282fe848ed954fa4ff8363eaa3f652102510668b90b3fad04d81889486ee1b641ee0d8c8b75cf32201f3b309e6b5fbb6cc869b48a91b62d3e +"byte-size@npm:^7.0.0": + version: 7.0.1 + resolution: "byte-size@npm:7.0.1" + checksum: 6791663a6d53bf950e896f119d3648fe8d7e8ae677e2ccdae84d0e5b78f21126e25f9d73aa19be2a297cb27abd36b6f5c361c0de36ebb2f3eb8a853f2ac99a4a languageName: node linkType: hard @@ -11684,7 +12323,7 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^16.1.0": +"cacache@npm:^16.0.0, cacache@npm:^16.0.6, cacache@npm:^16.1.0": version: 16.1.1 resolution: "cacache@npm:16.1.1" dependencies: @@ -11710,26 +12349,6 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^17.0.0": - version: 17.1.4 - resolution: "cacache@npm:17.1.4" - dependencies: - "@npmcli/fs": ^3.1.0 - fs-minipass: ^3.0.0 - glob: ^10.2.2 - lru-cache: ^7.7.1 - minipass: ^7.0.3 - minipass-collect: ^1.0.2 - minipass-flush: ^1.0.5 - minipass-pipeline: ^1.2.4 - p-map: ^4.0.0 - ssri: ^10.0.0 - tar: ^6.1.11 - unique-filename: ^3.0.0 - checksum: b7751df756656954a51201335addced8f63fc53266fa56392c9f5ae83c8d27debffb4458ac2d168a744a4517ec3f2163af05c20097f93d17bdc2dc8a385e14a6 - languageName: node - linkType: hard - "cachedir@npm:^2.3.0": version: 2.3.0 resolution: "cachedir@npm:2.3.0" @@ -12000,7 +12619,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:3.5.3, chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.3": +"chokidar@npm:3.5.3, chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -12052,10 +12671,17 @@ __metadata: languageName: node linkType: hard -"ci-info@npm:^3.2.0, ci-info@npm:^3.6.1": - version: 3.9.0 - resolution: "ci-info@npm:3.9.0" - checksum: 6b19dc9b2966d1f8c2041a838217299718f15d6c4b63ae36e4674edd2bee48f780e94761286a56aa59eb305a85fbea4ddffb7630ec063e7ec7e7e5ad42549a87 +"ci-info@npm:^2.0.0": + version: 2.0.0 + resolution: "ci-info@npm:2.0.0" + checksum: 3b374666a85ea3ca43fa49aa3a048d21c9b475c96eb13c133505d2324e7ae5efd6a454f41efe46a152269e9b6a00c9edbe63ec7fa1921957165aae16625acd67 + languageName: node + linkType: hard + +"ci-info@npm:^3.2.0": + version: 3.2.0 + resolution: "ci-info@npm:3.2.0" + checksum: c68995a94e95ce3f233ff845e62dfc56f2e8ff1e3f5c1361bcdd520cbbc9726d8a54cbc1a685cb9ee19c3c5e71a1dade6dda23eb364b59b8e6c32508a9b761bc languageName: node linkType: hard @@ -12180,7 +12806,7 @@ __metadata: languageName: node linkType: hard -"clone-deep@npm:4.0.1, clone-deep@npm:^4.0.1": +"clone-deep@npm:^4.0.1": version: 4.0.1 resolution: "clone-deep@npm:4.0.1" dependencies: @@ -12237,10 +12863,12 @@ __metadata: languageName: node linkType: hard -"cmd-shim@npm:6.0.1": - version: 6.0.1 - resolution: "cmd-shim@npm:6.0.1" - checksum: 359006b3a5bb4a0ff161a44ccc18fbba947db748ef0dd12273e476792e316a5edb0945d74bfa1e91cd88ce0511025fde87901eda092c479d83cfcd6734562683 +"cmd-shim@npm:^5.0.0": + version: 5.0.0 + resolution: "cmd-shim@npm:5.0.0" + dependencies: + mkdirp-infer-owner: ^2.0.0 + checksum: 83d2a46cdf4adbb38d3d3184364b2df0e4c001ac770f5ca94373825d7a48838b4cb8a59534ef48f02b0d556caa047728589ca65c640c17c0b417b3afb34acfbb languageName: node linkType: hard @@ -12353,7 +12981,7 @@ __metadata: languageName: node linkType: hard -"columnify@npm:1.6.0": +"columnify@npm:^1.6.0": version: 1.6.0 resolution: "columnify@npm:1.6.0" dependencies: @@ -12470,6 +13098,13 @@ __metadata: languageName: node linkType: hard +"common-ancestor-path@npm:^1.0.1": + version: 1.0.1 + resolution: "common-ancestor-path@npm:1.0.1" + checksum: 1d2e4186067083d8cc413f00fc2908225f04ae4e19417ded67faa6494fb313c4fcd5b28a52326d1a62b466e2b3a4325e92c31133c5fee628cdf8856b3a57c3d7 + languageName: node + linkType: hard + "common-path-prefix@npm:^3.0.0": version: 3.0.0 resolution: "common-path-prefix@npm:3.0.0" @@ -12572,6 +13207,16 @@ __metadata: languageName: node linkType: hard +"config-chain@npm:^1.1.12": + version: 1.1.13 + resolution: "config-chain@npm:1.1.13" + dependencies: + ini: ^1.3.4 + proto-list: ~1.2.1 + checksum: 828137a28e7c2fc4b7fb229bd0cd6c1397bcf83434de54347e608154008f411749041ee392cbe42fab6307e02de4c12480260bf769b7d44b778fdea3839eafab + languageName: node + linkType: hard + "connect-history-api-fallback@npm:^2.0.0": version: 2.0.0 resolution: "connect-history-api-fallback@npm:2.0.0" @@ -12609,96 +13254,105 @@ __metadata: languageName: node linkType: hard -"conventional-changelog-angular@npm:6.0.0": - version: 6.0.0 - resolution: "conventional-changelog-angular@npm:6.0.0" +"conventional-changelog-angular@npm:^5.0.12": + version: 5.0.13 + resolution: "conventional-changelog-angular@npm:5.0.13" dependencies: compare-func: ^2.0.0 - checksum: ddc59ead53a45b817d83208200967f5340866782b8362d5e2e34105fdfa3d3a31585ebbdec7750bdb9de53da869f847e8ca96634a9801f51e27ecf4e7ffe2bad + q: ^1.5.1 + checksum: 6ed4972fce25a50f9f038c749cc9db501363131b0fb2efc1fccecba14e4b1c80651d0d758d4c350a609f32010c66fa343eefd49c02e79e911884be28f53f3f90 languageName: node linkType: hard -"conventional-changelog-core@npm:5.0.1": - version: 5.0.1 - resolution: "conventional-changelog-core@npm:5.0.1" +"conventional-changelog-core@npm:^4.2.4": + version: 4.2.4 + resolution: "conventional-changelog-core@npm:4.2.4" dependencies: add-stream: ^1.0.0 - conventional-changelog-writer: ^6.0.0 - conventional-commits-parser: ^4.0.0 - dateformat: ^3.0.3 - get-pkg-repo: ^4.2.1 - git-raw-commits: ^3.0.0 + conventional-changelog-writer: ^5.0.0 + conventional-commits-parser: ^3.2.0 + dateformat: ^3.0.0 + get-pkg-repo: ^4.0.0 + git-raw-commits: ^2.0.8 git-remote-origin-url: ^2.0.0 - git-semver-tags: ^5.0.0 - normalize-package-data: ^3.0.3 + git-semver-tags: ^4.1.1 + lodash: ^4.17.15 + normalize-package-data: ^3.0.0 + q: ^1.5.1 read-pkg: ^3.0.0 read-pkg-up: ^3.0.0 - checksum: 5f37f14f8d5effb4c6bf861df11e918a277ecc2cf94534eaed44d1455b11ef450d0f6d122f0e7450a44a268d9473730cf918b7558964dcba2f0ac0896824e66f + through2: ^4.0.0 + checksum: 56d5194040495ea316e53fd64cb3614462c318f0fe54b1bf25aba6fba9b3d51cb9fdf7ac5b766f17e5529a3f90e317257394e00b0a9a5ce42caf3a59f82afb3a languageName: node linkType: hard -"conventional-changelog-preset-loader@npm:^3.0.0": - version: 3.0.0 - resolution: "conventional-changelog-preset-loader@npm:3.0.0" - checksum: 199c4730c5151f243d35c24585114900c2a7091eab5832cfeb49067a18a2b77d5c9a86b779e6e18b49278a1ff83c011c1d9bb6da95bd1f78d9e36d4d379216d5 +"conventional-changelog-preset-loader@npm:^2.3.4": + version: 2.3.4 + resolution: "conventional-changelog-preset-loader@npm:2.3.4" + checksum: 23a889b7fcf6fe7653e61f32a048877b2f954dcc1e0daa2848c5422eb908e6f24c78372f8d0d2130b5ed941c02e7010c599dccf44b8552602c6c8db9cb227453 languageName: node linkType: hard -"conventional-changelog-writer@npm:^6.0.0": - version: 6.0.1 - resolution: "conventional-changelog-writer@npm:6.0.1" +"conventional-changelog-writer@npm:^5.0.0": + version: 5.0.1 + resolution: "conventional-changelog-writer@npm:5.0.1" dependencies: - conventional-commits-filter: ^3.0.0 - dateformat: ^3.0.3 + conventional-commits-filter: ^2.0.7 + dateformat: ^3.0.0 handlebars: ^4.7.7 json-stringify-safe: ^5.0.1 - meow: ^8.1.2 - semver: ^7.0.0 - split: ^1.0.1 + lodash: ^4.17.15 + meow: ^8.0.0 + semver: ^6.0.0 + split: ^1.0.0 + through2: ^4.0.0 bin: conventional-changelog-writer: cli.js - checksum: d8619ff7446efa71e0a019c07bdf20debff3f32438f783277b80314109429d7075b3d913e59c57cd6e014e9bef611c2a8fb052de2832144f38c0e54485257126 + checksum: 5c0129db44577f14b1f8de225b62a392a9927ba7fe3422cb21ad71a771b8472bd03badb7c87cb47419913abc3f2ce3759b69f59550cdc6f7a7b0459015b3b44c languageName: node linkType: hard -"conventional-commits-filter@npm:^3.0.0": - version: 3.0.0 - resolution: "conventional-commits-filter@npm:3.0.0" +"conventional-commits-filter@npm:^2.0.7": + version: 2.0.7 + resolution: "conventional-commits-filter@npm:2.0.7" dependencies: lodash.ismatch: ^4.4.0 - modify-values: ^1.0.1 - checksum: 73337f42acff7189e1dfca8d13c9448ce085ac1c09976cb33617cc909949621befb1640b1c6c30a1be4953a1be0deea9e93fa0dc86725b8be8e249a64fbb4632 + modify-values: ^1.0.0 + checksum: feb567f680a6da1baaa1ef3cff393b3c56a5828f77ab9df5e70626475425d109a6fee0289b4979223c62bbd63bf9c98ef532baa6fcb1b66ee8b5f49077f5d46c languageName: node linkType: hard -"conventional-commits-parser@npm:^4.0.0": - version: 4.0.0 - resolution: "conventional-commits-parser@npm:4.0.0" +"conventional-commits-parser@npm:^3.2.0": + version: 3.2.4 + resolution: "conventional-commits-parser@npm:3.2.4" dependencies: - JSONStream: ^1.3.5 + JSONStream: ^1.0.4 is-text-path: ^1.0.1 - meow: ^8.1.2 - split2: ^3.2.2 + lodash: ^4.17.15 + meow: ^8.0.0 + split2: ^3.0.0 + through2: ^4.0.0 bin: conventional-commits-parser: cli.js - checksum: 12d95b5ba8e0710a6d3cd2e01f01dd7818fdf0bb2b33f4b75444e2c9aee49598776b0706a528ed49e83aec5f1896c32cbc7f8e6589f61a15187293707448f928 + checksum: 1627ff203bc9586d89e47a7fe63acecf339aba74903b9114e23d28094f79d4e2d6389bf146ae561461dcba8fc42e7bc228165d2b173f15756c43f1d32bc50bfd languageName: node linkType: hard -"conventional-recommended-bump@npm:7.0.1": - version: 7.0.1 - resolution: "conventional-recommended-bump@npm:7.0.1" +"conventional-recommended-bump@npm:^6.1.0": + version: 6.1.0 + resolution: "conventional-recommended-bump@npm:6.1.0" dependencies: concat-stream: ^2.0.0 - conventional-changelog-preset-loader: ^3.0.0 - conventional-commits-filter: ^3.0.0 - conventional-commits-parser: ^4.0.0 - git-raw-commits: ^3.0.0 - git-semver-tags: ^5.0.0 - meow: ^8.1.2 + conventional-changelog-preset-loader: ^2.3.4 + conventional-commits-filter: ^2.0.7 + conventional-commits-parser: ^3.2.0 + git-raw-commits: ^2.0.8 + git-semver-tags: ^4.1.1 + meow: ^8.0.0 + q: ^1.5.1 bin: conventional-recommended-bump: cli.js - checksum: e2d1f2f40f93612a6da035d0c1a12d70208e0da509a17a9c9296a05e73a6eca5d81fe8c6a7b45e973181fa7c876c6edb9a114a2d7da4f6df00c47c7684ab62d2 + checksum: da1d7a5f3b9f7706bede685cdcb3db67997fdaa43c310fd5bf340955c84a4b85dbb9427031522ee06dad290b730a54be987b08629d79c73720dbad3a2531146b languageName: node linkType: hard @@ -13751,7 +14405,7 @@ __metadata: languageName: node linkType: hard -"dateformat@npm:^3.0.3": +"dateformat@npm:^3.0.0": version: 3.0.3 resolution: "dateformat@npm:3.0.3" checksum: ca4911148abb09887bd9bdcd632c399b06f3ecad709a18eb594d289a1031982f441e08e281db77ffebcb2cbcbfa1ac578a7cbfbf8743f41009aa5adc1846ed34 @@ -13823,6 +14477,13 @@ __metadata: languageName: node linkType: hard +"debuglog@npm:^1.0.1": + version: 1.0.1 + resolution: "debuglog@npm:1.0.1" + checksum: 970679f2eb7a73867e04d45b52583e7ec6dee1f33c058e9147702e72a665a9647f9c3d6e7c2f66f6bf18510b23eb5ded1b617e48ac1db23603809c5ddbbb9763 + languageName: node + linkType: hard + "decamelize-keys@npm:^1.1.0": version: 1.1.0 resolution: "decamelize-keys@npm:1.1.0" @@ -13868,7 +14529,7 @@ __metadata: languageName: node linkType: hard -"dedent@npm:0.7.0, dedent@npm:^0.7.0": +"dedent@npm:^0.7.0": version: 0.7.0 resolution: "dedent@npm:0.7.0" checksum: 87de191050d9a40dd70cad01159a0bcf05ecb59750951242070b6abf9569088684880d00ba92a955b4058804f16eeaf91d604f283929b4f614d181cd7ae633d2 @@ -14036,7 +14697,7 @@ __metadata: languageName: node linkType: hard -"deprecation@npm:^2.0.0": +"deprecation@npm:^2.0.0, deprecation@npm:^2.3.1": version: 2.3.1 resolution: "deprecation@npm:2.3.1" checksum: f56a05e182c2c195071385455956b0c4106fe14e36245b00c689ceef8e8ab639235176a96977ba7c74afb173317fac2e0ec6ec7a1c6d1e6eaa401c586c714132 @@ -14064,7 +14725,7 @@ __metadata: languageName: node linkType: hard -"detect-indent@npm:^6.1.0": +"detect-indent@npm:^6.0.0, detect-indent@npm:^6.1.0": version: 6.1.0 resolution: "detect-indent@npm:6.1.0" checksum: ab953a73c72dbd4e8fc68e4ed4bfd92c97eb6c43734af3900add963fd3a9316f3bc0578b018b24198d4c31a358571eff5f0656e81a1f3b9ad5c547d58b2d093d @@ -14128,6 +14789,16 @@ __metadata: languageName: node linkType: hard +"dezalgo@npm:^1.0.0": + version: 1.0.4 + resolution: "dezalgo@npm:1.0.4" + dependencies: + asap: ^2.0.0 + wrappy: 1 + checksum: 895389c6aead740d2ab5da4d3466d20fa30f738010a4d3f4dcccc9fc645ca31c9d10b7e1804ae489b1eb02c7986f9f1f34ba132d409b043082a86d9a4e745624 + languageName: node + linkType: hard + "diff-sequences@npm:^26.6.2": version: 26.6.2 resolution: "diff-sequences@npm:26.6.2" @@ -14377,17 +15048,33 @@ __metadata: languageName: node linkType: hard -"dotenv-expand@npm:^10.0.0, dotenv-expand@npm:~10.0.0": +"dot-prop@npm:^6.0.1": + version: 6.0.1 + resolution: "dot-prop@npm:6.0.1" + dependencies: + is-obj: ^2.0.0 + checksum: 0f47600a4b93e1dc37261da4e6909652c008832a5d3684b5bf9a9a0d3f4c67ea949a86dceed9b72f5733ed8e8e6383cc5958df3bbd0799ee317fd181f2ece700 + languageName: node + linkType: hard + +"dotenv-expand@npm:^10.0.0": version: 10.0.0 resolution: "dotenv-expand@npm:10.0.0" checksum: 2a38b470efe0abcb1ac8490421a55e1d764dc9440fd220942bce40965074f3fb00b585f4346020cb0f0f219966ee6b4ee5023458b3e2953fe5b3214de1b314ee languageName: node linkType: hard -"dotenv@npm:^16.0.0, dotenv@npm:~16.3.1": - version: 16.3.1 - resolution: "dotenv@npm:16.3.1" - checksum: 15d75e7279018f4bafd0ee9706593dd14455ddb71b3bcba9c52574460b7ccaf67d5cf8b2c08a5af1a9da6db36c956a04a1192b101ee102a3e0cf8817bbcf3dfd +"dotenv@npm:^16.0.0": + version: 16.0.3 + resolution: "dotenv@npm:16.0.3" + checksum: afcf03f373d7a6d62c7e9afea6328e62851d627a4e73f2e12d0a8deae1cd375892004f3021883f8aec85932cd2834b091f568ced92b4774625b321db83b827f8 + languageName: node + linkType: hard + +"dotenv@npm:~10.0.0": + version: 10.0.0 + resolution: "dotenv@npm:10.0.0" + checksum: f412c5fe8c24fbe313d302d2500e247ba8a1946492db405a4de4d30dd0eb186a88a43f13c958c5a7de303938949c4231c56994f97d05c4bc1f22478d631b4005 languageName: node linkType: hard @@ -14441,7 +15128,7 @@ __metadata: languageName: node linkType: hard -"ejs@npm:^3.1.7, ejs@npm:^3.1.8": +"ejs@npm:^3.1.8": version: 3.1.9 resolution: "ejs@npm:3.1.9" dependencies: @@ -14605,7 +15292,7 @@ __metadata: languageName: node linkType: hard -"envinfo@npm:7.8.1, envinfo@npm:^7.7.3": +"envinfo@npm:^7.7.3, envinfo@npm:^7.7.4": version: 7.8.1 resolution: "envinfo@npm:7.8.1" bin: @@ -15665,23 +16352,6 @@ __metadata: languageName: node linkType: hard -"execa@npm:5.0.0": - version: 5.0.0 - resolution: "execa@npm:5.0.0" - dependencies: - cross-spawn: ^7.0.3 - get-stream: ^6.0.0 - human-signals: ^2.1.0 - is-stream: ^2.0.0 - merge-stream: ^2.0.0 - npm-run-path: ^4.0.1 - onetime: ^5.1.2 - signal-exit: ^3.0.3 - strip-final-newline: ^2.0.0 - checksum: a044367ebdcc68ca019810cb134510fc77bbc55c799122258ee0e00e289c132941ab48c2a331a036699c42bc8d479d451ae67c105fce5ce5cc813e7dd92d642b - languageName: node - linkType: hard - "execa@npm:5.1.1, execa@npm:^5.0.0, execa@npm:^5.1.1": version: 5.1.1 resolution: "execa@npm:5.1.1" @@ -15873,6 +16543,19 @@ __metadata: languageName: node linkType: hard +"fast-glob@npm:3.2.7": + version: 3.2.7 + resolution: "fast-glob@npm:3.2.7" + dependencies: + "@nodelib/fs.stat": ^2.0.2 + "@nodelib/fs.walk": ^1.2.3 + glob-parent: ^5.1.2 + merge2: ^1.3.0 + micromatch: ^4.0.4 + checksum: 2f4708ff112d2b451888129fdd9a0938db88b105b0ddfd043c064e3c4d3e20eed8d7c7615f7565fee660db34ddcf08a2db1bf0ab3c00b87608e4719694642d78 + languageName: node + linkType: hard + "fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.1": version: 3.3.1 resolution: "fast-glob@npm:3.3.1" @@ -16232,13 +16915,13 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.14.9, follow-redirects@npm:^1.15.0": - version: 1.15.3 - resolution: "follow-redirects@npm:1.15.3" +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.14.9": + version: 1.15.2 + resolution: "follow-redirects@npm:1.15.2" peerDependenciesMeta: debug: optional: true - checksum: 584da22ec5420c837bd096559ebfb8fe69d82512d5585004e36a3b4a6ef6d5905780e0c74508c7b72f907d1fa2b7bd339e613859e9c304d0dc96af2027fd0231 + checksum: faa66059b66358ba65c234c2f2a37fcec029dc22775f35d9ad6abac56003268baf41e55f9ee645957b32c7d9f62baf1f0b906e68267276f54ec4b4c597c2b190 languageName: node linkType: hard @@ -16376,7 +17059,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:11.1.1, fs-extra@npm:^11.1.0, fs-extra@npm:^11.1.1": +"fs-extra@npm:11.1.1, fs-extra@npm:^11.1.0": version: 11.1.1 resolution: "fs-extra@npm:11.1.1" dependencies: @@ -16387,7 +17070,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^10.0.0": +"fs-extra@npm:^10.0.0, fs-extra@npm:^10.1.0": version: 10.1.0 resolution: "fs-extra@npm:10.1.0" dependencies: @@ -16443,15 +17126,6 @@ __metadata: languageName: node linkType: hard -"fs-minipass@npm:^3.0.0": - version: 3.0.3 - resolution: "fs-minipass@npm:3.0.3" - dependencies: - minipass: ^7.0.3 - checksum: 8722a41109130851d979222d3ec88aabaceeaaf8f57b2a8f744ef8bd2d1ce95453b04a61daa0078822bc5cd21e008814f06fe6586f56fef511e71b8d2394d802 - languageName: node - linkType: hard - "fs-mkdirp-stream@npm:^1.0.0": version: 1.0.0 resolution: "fs-mkdirp-stream@npm:1.0.0" @@ -16645,7 +17319,7 @@ __metadata: languageName: node linkType: hard -"get-pkg-repo@npm:^4.2.1": +"get-pkg-repo@npm:^4.0.0": version: 4.2.1 resolution: "get-pkg-repo@npm:4.2.1" dependencies: @@ -16659,20 +17333,13 @@ __metadata: languageName: node linkType: hard -"get-port@npm:5.1.1, get-port@npm:^5.1.1": +"get-port@npm:^5.1.1": version: 5.1.1 resolution: "get-port@npm:5.1.1" checksum: 0162663ffe5c09e748cd79d97b74cd70e5a5c84b760a475ce5767b357fb2a57cb821cee412d646aa8a156ed39b78aab88974eddaa9e5ee926173c036c0713787 languageName: node linkType: hard -"get-stream@npm:6.0.0": - version: 6.0.0 - resolution: "get-stream@npm:6.0.0" - checksum: 587e6a93127f9991b494a566f4971cf7a2645dfa78034818143480a80587027bdd8826cdcf80d0eff4a4a19de0d231d157280f24789fc9cc31492e1dcc1290cf - languageName: node - linkType: hard - "get-stream@npm:^5.0.0, get-stream@npm:^5.1.0": version: 5.2.0 resolution: "get-stream@npm:5.2.0" @@ -16762,16 +17429,18 @@ __metadata: languageName: node linkType: hard -"git-raw-commits@npm:^3.0.0": - version: 3.0.0 - resolution: "git-raw-commits@npm:3.0.0" +"git-raw-commits@npm:^2.0.8": + version: 2.0.11 + resolution: "git-raw-commits@npm:2.0.11" dependencies: dargs: ^7.0.0 - meow: ^8.1.2 - split2: ^3.2.2 + lodash: ^4.17.15 + meow: ^8.0.0 + split2: ^3.0.0 + through2: ^4.0.0 bin: git-raw-commits: cli.js - checksum: 198892f307829d22fc8ec1c9b4a63876a1fde847763857bb74bd1b04c6f6bc0d7464340c25d0f34fd0fb395759363aa1f8ce324357027320d80523bf234676ab + checksum: c178af43633684106179793b6e3473e1d2bb50bb41d04e2e285ea4eef342ca4090fee6bc8a737552fde879d22346c90de5c49f18c719a0f38d4c934f258a0f79 languageName: node linkType: hard @@ -16785,15 +17454,15 @@ __metadata: languageName: node linkType: hard -"git-semver-tags@npm:^5.0.0": - version: 5.0.1 - resolution: "git-semver-tags@npm:5.0.1" +"git-semver-tags@npm:^4.1.1": + version: 4.1.1 + resolution: "git-semver-tags@npm:4.1.1" dependencies: - meow: ^8.1.2 - semver: ^7.0.0 + meow: ^8.0.0 + semver: ^6.0.0 bin: git-semver-tags: cli.js - checksum: c181e1d9e7649fd90e6c347f400f791db08b236265d79874dfa60f09ca893fa7a4fceebf3fd5f01443705e7eac5c73c5235eb96c6bc4a39eb37746a1d7c49ec4 + checksum: e16d02a515c0f88289a28b5bf59bf42c0dc053765922d3b617ae4b50546bd4f74a25bf3ad53b91cb6c1159319a2e92533b160c573b856c2629125c8b26b3b0e3 languageName: node linkType: hard @@ -16807,7 +17476,7 @@ __metadata: languageName: node linkType: hard -"git-url-parse@npm:13.1.0": +"git-url-parse@npm:^13.1.0": version: 13.1.0 resolution: "git-url-parse@npm:13.1.0" dependencies: @@ -16832,15 +17501,6 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:5.1.2, glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": - version: 5.1.2 - resolution: "glob-parent@npm:5.1.2" - dependencies: - is-glob: ^4.0.1 - checksum: f4f2bfe2425296e8a47e36864e4f42be38a996db40420fe434565e4480e3322f18eb37589617a98640c5dc8fdec1a387007ee18dbb1f3f5553409c34d17f425e - languageName: node - linkType: hard - "glob-parent@npm:^3.1.0": version: 3.1.0 resolution: "glob-parent@npm:3.1.0" @@ -16851,6 +17511,15 @@ __metadata: languageName: node linkType: hard +"glob-parent@npm:^5.1.1, glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": + version: 5.1.2 + resolution: "glob-parent@npm:5.1.2" + dependencies: + is-glob: ^4.0.1 + checksum: f4f2bfe2425296e8a47e36864e4f42be38a996db40420fe434565e4480e3322f18eb37589617a98640c5dc8fdec1a387007ee18dbb1f3f5553409c34d17f425e + languageName: node + linkType: hard + "glob-parent@npm:^6.0.1, glob-parent@npm:^6.0.2": version: 6.0.2 resolution: "glob-parent@npm:6.0.2" @@ -16928,7 +17597,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.2.5, glob@npm:^10.2.7": +"glob@npm:^10.0.0, glob@npm:^10.2.5, glob@npm:^10.2.7": version: 10.3.10 resolution: "glob@npm:10.3.10" dependencies: @@ -16970,18 +17639,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^9.2.0": - version: 9.3.5 - resolution: "glob@npm:9.3.5" - dependencies: - fs.realpath: ^1.0.0 - minimatch: ^8.0.2 - minipass: ^4.2.4 - path-scurry: ^1.6.1 - checksum: 94b093adbc591bc36b582f77927d1fb0dbf3ccc231828512b017601408be98d1fe798fc8c0b19c6f2d1a7660339c3502ce698de475e9d938ccbb69b47b647c84 - languageName: node - linkType: hard - "global-dirs@npm:^3.0.0": version: 3.0.1 resolution: "global-dirs@npm:3.0.1" @@ -17052,7 +17709,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:11.1.0, globby@npm:^11.0.1, globby@npm:^11.0.2, globby@npm:^11.1.0": +"globby@npm:^11.0.1, globby@npm:^11.0.2, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -17095,10 +17752,10 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:4.2.11, graceful-fs@npm:^4.0.0, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": - version: 4.2.11 - resolution: "graceful-fs@npm:4.2.11" - checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 +"graceful-fs@npm:^4.0.0, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": + version: 4.2.10 + resolution: "graceful-fs@npm:4.2.10" + checksum: 3f109d70ae123951905d85032ebeae3c2a5a7a997430df00ea30df0e3a6c60cf6689b109654d6fdacd28810a053348c4d14642da1d075049e6be1ba5216218da languageName: node linkType: hard @@ -17338,7 +17995,7 @@ __metadata: json-source-map: 0.6.1 jsurl: ^0.1.5 kbar: 0.1.0-beta.40 - lerna: 7.4.1 + lerna: 5.5.4 lodash: 4.17.21 logfmt: ^1.3.2 lru-cache: 10.0.0 @@ -17636,7 +18293,7 @@ __metadata: languageName: node linkType: hard -"has-unicode@npm:2.0.1, has-unicode@npm:^2.0.0, has-unicode@npm:^2.0.1": +"has-unicode@npm:^2.0.0, has-unicode@npm:^2.0.1": version: 2.0.1 resolution: "has-unicode@npm:2.0.1" checksum: 1eab07a7436512db0be40a710b29b5dc21fa04880b7f63c9980b706683127e3c1b57cb80ea96d47991bdae2dfe479604f6a1ba410106ee1046a41d1bd0814400 @@ -17801,12 +18458,12 @@ __metadata: languageName: node linkType: hard -"hosted-git-info@npm:^6.0.0": - version: 6.1.1 - resolution: "hosted-git-info@npm:6.1.1" +"hosted-git-info@npm:^5.0.0": + version: 5.0.0 + resolution: "hosted-git-info@npm:5.0.0" dependencies: lru-cache: ^7.5.1 - checksum: fcd3ca2eaa05f3201425ccbb8aa47f88cdda4a3a6d79453f8e269f7171356278bd1db08f059d8439eb5eaa91c6a8a20800fc49cca6e9e4e899b202a332d5ba6b + checksum: 515e69463d123635f70d70656c5ec648951ffc1987f92a87cb4a038e1794bfed833cf87569b358b137ebbc75d992c073ed0408d420c9e5b717c2b4f0a291490c languageName: node linkType: hard @@ -17956,7 +18613,7 @@ __metadata: languageName: node linkType: hard -"http-cache-semantics@npm:^4.1.0, http-cache-semantics@npm:^4.1.1": +"http-cache-semantics@npm:^4.1.0": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" checksum: 83ac0bc60b17a3a36f9953e7be55e5c8f41acc61b22583060e8dedc9dd5e3607c823a88d0926f9150e571f90946835c7fe150732801010845c72cd8bbff1a236 @@ -18246,15 +18903,6 @@ __metadata: languageName: node linkType: hard -"ignore-walk@npm:^6.0.0": - version: 6.0.3 - resolution: "ignore-walk@npm:6.0.3" - dependencies: - minimatch: ^9.0.0 - checksum: d8ba534beb3a3fa48ddd32c79bbedb14a831ff7fab548674765d661d8f8d0df4b0827e3ad86e35cb15ff027655bfd6a477bd8d5d0411e229975a7c716f1fc9de - languageName: node - linkType: hard - "ignore@npm:^3.3.10": version: 3.3.10 resolution: "ignore@npm:3.3.10" @@ -18314,15 +18962,15 @@ __metadata: languageName: node linkType: hard -"import-local@npm:3.1.0, import-local@npm:^3.0.2": - version: 3.1.0 - resolution: "import-local@npm:3.1.0" +"import-local@npm:^3.0.2": + version: 3.0.3 + resolution: "import-local@npm:3.0.3" dependencies: pkg-dir: ^4.2.0 resolve-cwd: ^3.0.0 bin: import-local-fixture: fixtures/cli.js - checksum: bfcdb63b5e3c0e245e347f3107564035b128a414c4da1172a20dc67db2504e05ede4ac2eee1252359f78b0bfd7b19ef180aec427c2fce6493ae782d73a04cddd + checksum: 38ae57d35e7fd5f63b55895050c798d4dd590e4e2337e9ffa882fb3ea7a7716f3162c7300e382e0a733ca5d07b389fadff652c00fa7b072d5cb6ea34ca06b179 languageName: node linkType: hard @@ -18385,25 +19033,25 @@ __metadata: languageName: node linkType: hard -"ini@npm:^1.3.2, ini@npm:^1.3.5, ini@npm:^1.3.8": +"ini@npm:^1.3.2, ini@npm:^1.3.4, ini@npm:^1.3.5": version: 1.3.8 resolution: "ini@npm:1.3.8" checksum: dfd98b0ca3a4fc1e323e38a6c8eb8936e31a97a918d3b377649ea15bdb15d481207a0dda1021efbd86b464cae29a0d33c1d7dcaf6c5672bee17fa849bc50a1b3 languageName: node linkType: hard -"init-package-json@npm:5.0.0": - version: 5.0.0 - resolution: "init-package-json@npm:5.0.0" +"init-package-json@npm:^3.0.2": + version: 3.0.2 + resolution: "init-package-json@npm:3.0.2" dependencies: - npm-package-arg: ^10.0.0 - promzard: ^1.0.0 - read: ^2.0.0 - read-package-json: ^6.0.0 + npm-package-arg: ^9.0.1 + promzard: ^0.3.0 + read: ^1.0.7 + read-package-json: ^5.0.0 semver: ^7.3.5 validate-npm-package-license: ^3.0.4 - validate-npm-package-name: ^5.0.0 - checksum: ad601c717d5ea3ff5a416cbe7d39417bb3914596dce7a386bffe856229435ebef06eb600736326effdd4e57a02d41164aa525d31d51ec49812c8e8c215d1d7c8 + validate-npm-package-name: ^4.0.0 + checksum: e027f60e4a1564809eee790d5a842341c784888fd7c7ace5f9a34ea76224c0adb6f3ab3bf205cf1c9c877a6e1a76c68b00847a984139f60813125d7b42a23a13 languageName: node linkType: hard @@ -18635,7 +19283,18 @@ __metadata: languageName: node linkType: hard -"is-ci@npm:3.0.1, is-ci@npm:^3.0.0": +"is-ci@npm:^2.0.0": + version: 2.0.0 + resolution: "is-ci@npm:2.0.0" + dependencies: + ci-info: ^2.0.0 + bin: + is-ci: bin.js + checksum: 77b869057510f3efa439bbb36e9be429d53b3f51abd4776eeea79ab3b221337fe1753d1e50058a9e2c650d38246108beffb15ccfd443929d77748d8c0cc90144 + languageName: node + linkType: hard + +"is-ci@npm:^3.0.0": version: 3.0.1 resolution: "is-ci@npm:3.0.1" dependencies: @@ -18908,7 +19567,7 @@ __metadata: languageName: node linkType: hard -"is-plain-obj@npm:^2.1.0": +"is-plain-obj@npm:^2.0.0, is-plain-obj@npm:^2.1.0": version: 2.1.0 resolution: "is-plain-obj@npm:2.1.0" checksum: cec9100678b0a9fe0248a81743041ed990c2d4c99f893d935545cfbc42876cbe86d207f3b895700c690ad2fa520e568c44afc1605044b535a7820c1d40e38daa @@ -19012,13 +19671,6 @@ __metadata: languageName: node linkType: hard -"is-stream@npm:2.0.0": - version: 2.0.0 - resolution: "is-stream@npm:2.0.0" - checksum: 4dc47738e26bc4f1b3be9070b6b9e39631144f204fc6f87db56961220add87c10a999ba26cf81699f9ef9610426f69cb08a4713feff8deb7d8cadac907826935 - languageName: node - linkType: hard - "is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" @@ -19062,7 +19714,7 @@ __metadata: languageName: node linkType: hard -"is-typedarray@npm:~1.0.0": +"is-typedarray@npm:^1.0.0, is-typedarray@npm:~1.0.0": version: 1.0.0 resolution: "is-typedarray@npm:1.0.0" checksum: 3508c6cd0a9ee2e0df2fa2e9baabcdc89e911c7bd5cf64604586697212feec525aa21050e48affb5ffc3df20f0f5d2e2cf79b08caa64e1ccc9578e251763aef7 @@ -19422,18 +20074,6 @@ __metadata: languageName: node linkType: hard -"jest-diff@npm:>=29.4.3 < 30, jest-diff@npm:^29.4.1, jest-diff@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-diff@npm:29.7.0" - dependencies: - chalk: ^4.0.0 - diff-sequences: ^29.6.3 - jest-get-type: ^29.6.3 - pretty-format: ^29.7.0 - checksum: 08e24a9dd43bfba1ef07a6374e5af138f53137b79ec3d5cc71a2303515335898888fa5409959172e1e05de966c9e714368d15e8994b0af7441f0721ee8e1bb77 - languageName: node - linkType: hard - "jest-diff@npm:^26.0.0": version: 26.6.2 resolution: "jest-diff@npm:26.6.2" @@ -19458,6 +20098,18 @@ __metadata: languageName: node linkType: hard +"jest-diff@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-diff@npm:29.7.0" + dependencies: + chalk: ^4.0.0 + diff-sequences: ^29.6.3 + jest-get-type: ^29.6.3 + pretty-format: ^29.7.0 + checksum: 08e24a9dd43bfba1ef07a6374e5af138f53137b79ec3d5cc71a2303515335898888fa5409959172e1e05de966c9e714368d15e8994b0af7441f0721ee8e1bb77 + languageName: node + linkType: hard + "jest-docblock@npm:^29.7.0": version: 29.7.0 resolution: "jest-docblock@npm:29.7.0" @@ -20117,13 +20769,6 @@ __metadata: languageName: node linkType: hard -"json-parse-even-better-errors@npm:^3.0.0": - version: 3.0.0 - resolution: "json-parse-even-better-errors@npm:3.0.0" - checksum: f1970b5220c7fa23d888565510752c3d5e863f93668a202fcaa719739fa41485dfc6a1db212f702ebd3c873851cc067aebc2917e3f79763cae2fdb95046f38f3 - languageName: node - linkType: hard - "json-schema-traverse@npm:^0.4.1": version: 0.4.1 resolution: "json-schema-traverse@npm:0.4.1" @@ -20159,6 +20804,13 @@ __metadata: languageName: node linkType: hard +"json-stringify-nice@npm:^1.1.4": + version: 1.1.4 + resolution: "json-stringify-nice@npm:1.1.4" + checksum: 6ddf781148b46857ab04e97f47be05f14c4304b86eb5478369edbeacd070c21c697269964b982fc977e8989d4c59091103b1d9dc291aba40096d6cbb9a392b72 + languageName: node + linkType: hard + "json-stringify-pretty-compact@npm:^2.0.0": version: 2.0.0 resolution: "json-stringify-pretty-compact@npm:2.0.0" @@ -20184,7 +20836,7 @@ __metadata: languageName: node linkType: hard -"json5@npm:^2.1.2, json5@npm:^2.2.2, json5@npm:^2.2.3": +"json5@npm:^2.1.2, json5@npm:^2.2.3": version: 2.2.3 resolution: "json5@npm:2.2.3" bin: @@ -20298,6 +20950,20 @@ __metadata: languageName: node linkType: hard +"just-diff-apply@npm:^5.2.0": + version: 5.3.1 + resolution: "just-diff-apply@npm:5.3.1" + checksum: c864606096f2506f043f90c58196bf47344b4c60e97171ea6ec3430e4664aa2eddc6722ff87c66fef4d6d6b47364b053f90a10d59319135a6c06ba5dd424b58e + languageName: node + linkType: hard + +"just-diff@npm:^5.0.1": + version: 5.0.3 + resolution: "just-diff@npm:5.0.3" + checksum: 89e5c3deb0525e8d5f651a0775ca62807d8924e386c3ab58d81ac7392ac10f6c98c677ea6e5578618e483fc88139e7ebde1c4130296e83d802ac3103f7e210cd + languageName: node + linkType: hard + "kbar@npm:0.1.0-beta.40": version: 0.1.0-beta.40 resolution: "kbar@npm:0.1.0-beta.40" @@ -20438,88 +21104,33 @@ __metadata: languageName: node linkType: hard -"lerna@npm:7.4.1": - version: 7.4.1 - resolution: "lerna@npm:7.4.1" +"lerna@npm:5.5.4": + version: 5.5.4 + resolution: "lerna@npm:5.5.4" dependencies: - "@lerna/child-process": 7.4.1 - "@lerna/create": 7.4.1 - "@npmcli/run-script": 6.0.2 - "@nx/devkit": ">=16.5.1 < 17" - "@octokit/plugin-enterprise-rest": 6.0.1 - "@octokit/rest": 19.0.11 - byte-size: 8.1.1 - chalk: 4.1.0 - clone-deep: 4.0.1 - cmd-shim: 6.0.1 - columnify: 1.6.0 - conventional-changelog-angular: 6.0.0 - conventional-changelog-core: 5.0.1 - conventional-recommended-bump: 7.0.1 - cosmiconfig: ^8.2.0 - dedent: 0.7.0 - envinfo: 7.8.1 - execa: 5.0.0 - fs-extra: ^11.1.1 - get-port: 5.1.1 - get-stream: 6.0.0 - git-url-parse: 13.1.0 - glob-parent: 5.1.2 - globby: 11.1.0 - graceful-fs: 4.2.11 - has-unicode: 2.0.1 - import-local: 3.1.0 - ini: ^1.3.8 - init-package-json: 5.0.0 - inquirer: ^8.2.4 - is-ci: 3.0.1 - is-stream: 2.0.0 - jest-diff: ">=29.4.3 < 30" - js-yaml: 4.1.0 - libnpmaccess: 7.0.2 - libnpmpublish: 7.3.0 - load-json-file: 6.2.0 - lodash: ^4.17.21 - make-dir: 4.0.0 - minimatch: 3.0.5 - multimatch: 5.0.0 - node-fetch: 2.6.7 - npm-package-arg: 8.1.1 - npm-packlist: 5.1.1 - npm-registry-fetch: ^14.0.5 + "@lerna/add": 5.5.4 + "@lerna/bootstrap": 5.5.4 + "@lerna/changed": 5.5.4 + "@lerna/clean": 5.5.4 + "@lerna/cli": 5.5.4 + "@lerna/create": 5.5.4 + "@lerna/diff": 5.5.4 + "@lerna/exec": 5.5.4 + "@lerna/import": 5.5.4 + "@lerna/info": 5.5.4 + "@lerna/init": 5.5.4 + "@lerna/link": 5.5.4 + "@lerna/list": 5.5.4 + "@lerna/publish": 5.5.4 + "@lerna/run": 5.5.4 + "@lerna/version": 5.5.4 + import-local: ^3.0.2 npmlog: ^6.0.2 - nx: ">=16.5.1 < 17" - p-map: 4.0.0 - p-map-series: 2.1.0 - p-pipe: 3.1.0 - p-queue: 6.6.2 - p-reduce: 2.1.0 - p-waterfall: 2.1.1 - pacote: ^15.2.0 - pify: 5.0.0 - read-cmd-shim: 4.0.0 - read-package-json: 6.0.4 - resolve-from: 5.0.0 - rimraf: ^4.4.1 - semver: ^7.3.8 - signal-exit: 3.0.7 - slash: 3.0.0 - ssri: ^9.0.1 - strong-log-transformer: 2.1.0 - tar: 6.1.11 - temp-dir: 1.0.0 - typescript: ">=3 < 6" - upath: 2.0.1 - uuid: ^9.0.0 - validate-npm-package-license: 3.0.4 - validate-npm-package-name: 5.0.0 - write-file-atomic: 5.0.1 - write-pkg: 4.0.0 - yargs: 16.2.0 - yargs-parser: 20.2.4 + nx: ">=14.6.1 < 16" + typescript: ^3 || ^4 bin: - lerna: dist/cli.js - checksum: 5670bc36c9db19ae3bad13828e30dc0be53966bb86e78309477de5d1f866eef96f195cf9494a0de940e6403992189bbf7dbc21405b695c1a9a5dafe4aa5ae43c + lerna: cli.js + checksum: 3107df46a5ce9d5bc4c5587767ac7b27d62b9732991853c48a58c88bdbc8ad972df7928e5cf98fbcb4d87ba5e47116cec5326cb9438f235e7a6686bc4e1c9ada languageName: node linkType: hard @@ -20540,29 +21151,28 @@ __metadata: languageName: node linkType: hard -"libnpmaccess@npm:7.0.2": - version: 7.0.2 - resolution: "libnpmaccess@npm:7.0.2" +"libnpmaccess@npm:^6.0.3": + version: 6.0.4 + resolution: "libnpmaccess@npm:6.0.4" dependencies: - npm-package-arg: ^10.1.0 - npm-registry-fetch: ^14.0.3 - checksum: 73d49f39391173276c46c12e32f503709338efd867d255d062ae9bc9e9f464d61240747f42bdd6dc6003a5dc275a27352ebfc11ed4cb424091463f302d823f23 + aproba: ^2.0.0 + minipass: ^3.1.1 + npm-package-arg: ^9.0.1 + npm-registry-fetch: ^13.0.0 + checksum: 86130b435c67a03254489c3b3684d435260b609164f76bcc69adbee78652c36a64551228b2c5ddc2b16851e9e367ee0ba173a641406768397716faa006042322 languageName: node linkType: hard -"libnpmpublish@npm:7.3.0": - version: 7.3.0 - resolution: "libnpmpublish@npm:7.3.0" +"libnpmpublish@npm:^6.0.4": + version: 6.0.5 + resolution: "libnpmpublish@npm:6.0.5" dependencies: - ci-info: ^3.6.1 - normalize-package-data: ^5.0.0 - npm-package-arg: ^10.1.0 - npm-registry-fetch: ^14.0.3 - proc-log: ^3.0.0 + normalize-package-data: ^4.0.0 + npm-package-arg: ^9.0.1 + npm-registry-fetch: ^13.0.0 semver: ^7.3.7 - sigstore: ^1.4.0 - ssri: ^10.0.1 - checksum: 03bedb65eb2293cfe5039f925ec1041deea698c5ac802bb74f6a0d44ee70529c38c32eea7c722f3a1f1219b54314021ad7f4764f93b66d619bea62ce0759faa0 + ssri: ^9.0.0 + checksum: d2f2434517038438be44db2e90e1c8c524df05f7c3b1458617177c2f9ca008dde8a72a4f739b34aee4df0352f71c9289788da86aa38a4709e05c6db33eed570a languageName: node linkType: hard @@ -20580,7 +21190,7 @@ __metadata: languageName: node linkType: hard -"lines-and-columns@npm:^2.0.3, lines-and-columns@npm:~2.0.3": +"lines-and-columns@npm:^2.0.3": version: 2.0.3 resolution: "lines-and-columns@npm:2.0.3" checksum: 5955363dfd7d3d7c476d002eb47944dbe0310d57959e2112dce004c0dc76cecfd479cf8c098fd479ff344acdf04ee0e82b455462a26492231ac152f6c48d17a1 @@ -20608,18 +21218,6 @@ __metadata: languageName: node linkType: hard -"load-json-file@npm:6.2.0": - version: 6.2.0 - resolution: "load-json-file@npm:6.2.0" - dependencies: - graceful-fs: ^4.1.15 - parse-json: ^5.0.0 - strip-bom: ^4.0.0 - type-fest: ^0.6.0 - checksum: 4429e430ebb99375fc7cd936348e4f7ba729486080ced4272091c1e386a7f5f738ea3337d8ffd4b01c2f5bc3ddde92f2c780045b66838fe98bdb79f901884643 - languageName: node - linkType: hard - "load-json-file@npm:^4.0.0": version: 4.0.0 resolution: "load-json-file@npm:4.0.0" @@ -20632,6 +21230,18 @@ __metadata: languageName: node linkType: hard +"load-json-file@npm:^6.2.0": + version: 6.2.0 + resolution: "load-json-file@npm:6.2.0" + dependencies: + graceful-fs: ^4.1.15 + parse-json: ^5.0.0 + strip-bom: ^4.0.0 + type-fest: ^0.6.0 + checksum: 4429e430ebb99375fc7cd936348e4f7ba729486080ced4272091c1e386a7f5f738ea3337d8ffd4b01c2f5bc3ddde92f2c780045b66838fe98bdb79f901884643 + languageName: node + linkType: hard + "loader-runner@npm:^4.2.0": version: 4.2.0 resolution: "loader-runner@npm:4.2.0" @@ -20950,15 +21560,6 @@ __metadata: languageName: node linkType: hard -"make-dir@npm:4.0.0": - version: 4.0.0 - resolution: "make-dir@npm:4.0.0" - dependencies: - semver: ^7.5.3 - checksum: bf0731a2dd3aab4db6f3de1585cea0b746bb73eb5a02e3d8d72757e376e64e6ada190b1eddcde5b2f24a81b688a9897efd5018737d05e02e2a671dda9cff8a8a - languageName: node - linkType: hard - "make-dir@npm:^2.0.0, make-dir@npm:^2.1.0": version: 2.1.0 resolution: "make-dir@npm:2.1.0" @@ -20985,7 +21586,7 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^10.0.3": +"make-fetch-happen@npm:^10.0.3, make-fetch-happen@npm:^10.0.6": version: 10.1.8 resolution: "make-fetch-happen@npm:10.1.8" dependencies: @@ -21009,29 +21610,6 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^11.0.0, make-fetch-happen@npm:^11.0.1, make-fetch-happen@npm:^11.1.1": - version: 11.1.1 - resolution: "make-fetch-happen@npm:11.1.1" - dependencies: - agentkeepalive: ^4.2.1 - cacache: ^17.0.0 - http-cache-semantics: ^4.1.1 - http-proxy-agent: ^5.0.0 - https-proxy-agent: ^5.0.0 - is-lambda: ^1.0.1 - lru-cache: ^7.7.1 - minipass: ^5.0.0 - minipass-fetch: ^3.0.0 - minipass-flush: ^1.0.5 - minipass-pipeline: ^1.2.4 - negotiator: ^0.6.3 - promise-retry: ^2.0.1 - socks-proxy-agent: ^7.0.0 - ssri: ^10.0.0 - checksum: 7268bf274a0f6dcf0343829489a4506603ff34bd0649c12058753900b0eb29191dce5dba12680719a5d0a983d3e57810f594a12f3c18494e93a1fbc6348a4540 - languageName: node - linkType: hard - "make-fetch-happen@npm:^9.1.0": version: 9.1.0 resolution: "make-fetch-happen@npm:9.1.0" @@ -21250,7 +21828,7 @@ __metadata: languageName: node linkType: hard -"meow@npm:^8.1.2": +"meow@npm:^8.0.0": version: 8.1.2 resolution: "meow@npm:8.1.2" dependencies: @@ -21420,21 +21998,12 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^8.0.2": - version: 8.0.4 - resolution: "minimatch@npm:8.0.4" +"minimatch@npm:^9.0.1": + version: 9.0.1 + resolution: "minimatch@npm:9.0.1" dependencies: brace-expansion: ^2.0.1 - checksum: 2e46cffb86bacbc524ad45a6426f338920c529dd13f3a732cc2cf7618988ee1aae88df4ca28983285aca9e0f45222019ac2d14ebd17c1edadd2ee12221ab801a - languageName: node - linkType: hard - -"minimatch@npm:^9.0.0, minimatch@npm:^9.0.1": - version: 9.0.3 - resolution: "minimatch@npm:9.0.3" - dependencies: - brace-expansion: ^2.0.1 - checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 + checksum: 97f5f5284bb57dc65b9415dec7f17a0f6531a33572193991c60ff18450dcfad5c2dad24ffeaf60b5261dccd63aae58cc3306e2209d57e7f88c51295a532d8ec3 languageName: node linkType: hard @@ -21495,21 +22064,6 @@ __metadata: languageName: node linkType: hard -"minipass-fetch@npm:^3.0.0": - version: 3.0.4 - resolution: "minipass-fetch@npm:3.0.4" - dependencies: - encoding: ^0.1.13 - minipass: ^7.0.3 - minipass-sized: ^1.0.3 - minizlib: ^2.1.2 - dependenciesMeta: - encoding: - optional: true - checksum: af7aad15d5c128ab1ebe52e043bdf7d62c3c6f0cecb9285b40d7b395e1375b45dcdfd40e63e93d26a0e8249c9efd5c325c65575aceee192883970ff8cb11364a - languageName: node - linkType: hard - "minipass-flush@npm:^1.0.5": version: 1.0.5 resolution: "minipass-flush@npm:1.0.5" @@ -21556,24 +22110,17 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^4.0.0, minipass@npm:^4.2.4": - version: 4.2.8 - resolution: "minipass@npm:4.2.8" - checksum: 7f4914d5295a9a30807cae5227a37a926e6d910c03f315930fde52332cf0575dfbc20295318f91f0baf0e6bb11a6f668e30cde8027dea7a11b9d159867a3c830 +"minipass@npm:^4.0.0": + version: 4.2.5 + resolution: "minipass@npm:4.2.5" + checksum: 4f9c19af23a5d4a9e7156feefc9110634b178a8cff8f8271af16ec5ebf7e221725a97429952c856f5b17b30c2065ebd24c81722d90c93d2122611d75b952b48f languageName: node linkType: hard -"minipass@npm:^5.0.0": - version: 5.0.0 - resolution: "minipass@npm:5.0.0" - checksum: 425dab288738853fded43da3314a0b5c035844d6f3097a8e3b5b29b328da8f3c1af6fc70618b32c29ff906284cf6406b6841376f21caaadd0793c1d5a6a620ea - languageName: node - linkType: hard - -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.3": - version: 7.0.4 - resolution: "minipass@npm:7.0.4" - checksum: 87585e258b9488caf2e7acea242fd7856bbe9a2c84a7807643513a338d66f368c7d518200ad7b70a508664d408aa000517647b2930c259a8b1f9f0984f344a21 +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0": + version: 7.0.2 + resolution: "minipass@npm:7.0.2" + checksum: 46776de732eb7cef2c7404a15fb28c41f5c54a22be50d47b03c605bf21f5c18d61a173c0a20b49a97e7a65f78d887245066410642551e45fffe04e9ac9e325bc languageName: node linkType: hard @@ -21594,6 +22141,17 @@ __metadata: languageName: node linkType: hard +"mkdirp-infer-owner@npm:^2.0.0": + version: 2.0.0 + resolution: "mkdirp-infer-owner@npm:2.0.0" + dependencies: + chownr: ^2.0.0 + infer-owner: ^1.0.4 + mkdirp: ^1.0.3 + checksum: d8f4ecd32f6762459d6b5714eae6487c67ae9734ab14e26d14377ddd9b2a1bf868d8baa18c0f3e73d3d513f53ec7a698e0f81a9367102c870a55bef7833880f7 + languageName: node + linkType: hard + "mkdirp@npm:^0.5.1, mkdirp@npm:^0.5.4, mkdirp@npm:^0.5.6": version: 0.5.6 resolution: "mkdirp@npm:0.5.6" @@ -21662,7 +22220,7 @@ __metadata: languageName: node linkType: hard -"modify-values@npm:^1.0.1": +"modify-values@npm:^1.0.0": version: 1.0.1 resolution: "modify-values@npm:1.0.1" checksum: 8296610c608bc97b03c2cf889c6cdf4517e32fa2d836440096374c2209f6b7b3e256c209493a0b32584b9cb32d528e99d0dd19dcd9a14d2d915a312d391cc7e9 @@ -21817,7 +22375,7 @@ __metadata: languageName: node linkType: hard -"multimatch@npm:5.0.0": +"multimatch@npm:^5.0.0": version: 5.0.0 resolution: "multimatch@npm:5.0.0" dependencies: @@ -21844,20 +22402,13 @@ __metadata: languageName: node linkType: hard -"mute-stream@npm:0.0.8": +"mute-stream@npm:0.0.8, mute-stream@npm:~0.0.4": version: 0.0.8 resolution: "mute-stream@npm:0.0.8" checksum: ff48d251fc3f827e5b1206cda0ffdaec885e56057ee86a3155e1951bc940fd5f33531774b1cc8414d7668c10a8907f863f6561875ee6e8768931a62121a531a1 languageName: node linkType: hard -"mute-stream@npm:~1.0.0": - version: 1.0.0 - resolution: "mute-stream@npm:1.0.0" - checksum: 36fc968b0e9c9c63029d4f9dc63911950a3bdf55c9a87f58d3a266289b67180201cade911e7699f8b2fa596b34c9db43dad37649e3f7fdd13c3bb9edb0017ee7 - languageName: node - linkType: hard - "nano-css@npm:^5.3.1": version: 5.3.4 resolution: "nano-css@npm:5.3.4" @@ -21975,21 +22526,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:2.6.7": - version: 2.6.7 - resolution: "node-fetch@npm:2.6.7" - dependencies: - whatwg-url: ^5.0.0 - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - checksum: 8d816ffd1ee22cab8301c7756ef04f3437f18dace86a1dae22cf81db8ef29c0bf6655f3215cb0cdb22b420b6fe141e64b26905e7f33f9377a7fa59135ea3e10b - languageName: node - linkType: hard - -"node-fetch@npm:^2.0.0, node-fetch@npm:^2.6.7": +"node-fetch@npm:^2.0.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7": version: 2.6.12 resolution: "node-fetch@npm:2.6.12" dependencies: @@ -22068,13 +22605,6 @@ __metadata: languageName: node linkType: hard -"node-machine-id@npm:1.1.12": - version: 1.1.12 - resolution: "node-machine-id@npm:1.1.12" - checksum: e23088a0fb4a77a1d6484b7f09a22992fd3e0054d4f2e427692b4c7081e6cf30118ba07b6113b6c89f1ce46fd26ec5ab1d76dcaf6c10317717889124511283a5 - languageName: node - linkType: hard - "node-notifier@npm:10.0.1": version: 10.0.1 resolution: "node-notifier@npm:10.0.1" @@ -22119,7 +22649,7 @@ __metadata: languageName: node linkType: hard -"normalize-package-data@npm:^3.0.0, normalize-package-data@npm:^3.0.2, normalize-package-data@npm:^3.0.3": +"normalize-package-data@npm:^3.0.0, normalize-package-data@npm:^3.0.2": version: 3.0.3 resolution: "normalize-package-data@npm:3.0.3" dependencies: @@ -22131,15 +22661,15 @@ __metadata: languageName: node linkType: hard -"normalize-package-data@npm:^5.0.0": - version: 5.0.0 - resolution: "normalize-package-data@npm:5.0.0" +"normalize-package-data@npm:^4.0.0": + version: 4.0.0 + resolution: "normalize-package-data@npm:4.0.0" dependencies: - hosted-git-info: ^6.0.0 + hosted-git-info: ^5.0.0 is-core-module: ^2.8.1 semver: ^7.3.5 validate-npm-package-license: ^3.0.4 - checksum: a459f05eaf7c2b643c61234177f08e28064fde97da15800e3d3ac0404e28450d43ac46fc95fbf6407a9bf20af4c58505ad73458a912dc1517f8c1687b1d68c27 + checksum: b0f47de4295a0f8499bd478e84b9f9592a29f65227c2b4446ae80f7dff6e7a5ec6ef25ea8f06f3dcb9b7b7d945c2daa274385925b3d85e77e34eaffa0b42e316 languageName: node linkType: hard @@ -22175,7 +22705,7 @@ __metadata: languageName: node linkType: hard -"npm-bundled@npm:^1.1.2": +"npm-bundled@npm:^1.1.1, npm-bundled@npm:^1.1.2": version: 1.1.2 resolution: "npm-bundled@npm:1.1.2" dependencies: @@ -22184,35 +22714,26 @@ __metadata: languageName: node linkType: hard -"npm-bundled@npm:^3.0.0": - version: 3.0.0 - resolution: "npm-bundled@npm:3.0.0" - dependencies: - npm-normalize-package-bin: ^3.0.0 - checksum: 110859c2d6dcd7941dac0932a29171cbde123060486a4b6e897aaf5e025abeb3d9ffcdfe9e9271992e6396b2986c2c534f1029a45a7c196f1257fa244305dbf8 - languageName: node - linkType: hard - -"npm-install-checks@npm:^6.0.0": - version: 6.3.0 - resolution: "npm-install-checks@npm:6.3.0" +"npm-install-checks@npm:^5.0.0": + version: 5.0.0 + resolution: "npm-install-checks@npm:5.0.0" dependencies: semver: ^7.1.1 - checksum: 6c20dadb878a0d2f1f777405217b6b63af1299d0b43e556af9363ee6eefaa98a17dfb7b612a473a473e96faf7e789c58b221e0d8ffdc1d34903c4f71618df3b4 + checksum: 0e7d1aae52b1fe9d3a0fd4a008850c7047931722dd49ee908afd13fd0297ac5ddb10964d9c59afcdaaa2ca04b51d75af2788f668c729ae71fec0e4cdac590ffc languageName: node linkType: hard -"npm-normalize-package-bin@npm:^1.0.1": +"npm-normalize-package-bin@npm:^1.0.0, npm-normalize-package-bin@npm:^1.0.1": version: 1.0.1 resolution: "npm-normalize-package-bin@npm:1.0.1" checksum: ae7f15155a1e3ace2653f12ddd1ee8eaa3c84452fdfbf2f1943e1de264e4b079c86645e2c55931a51a0a498cba31f70022a5219d5665fbcb221e99e58bc70122 languageName: node linkType: hard -"npm-normalize-package-bin@npm:^3.0.0": - version: 3.0.1 - resolution: "npm-normalize-package-bin@npm:3.0.1" - checksum: de416d720ab22137a36292ff8a333af499ea0933ef2320a8c6f56a73b0f0448227fec4db5c890d702e26d21d04f271415eab6580b5546456861cc0c19498a4bf +"npm-normalize-package-bin@npm:^2.0.0": + version: 2.0.0 + resolution: "npm-normalize-package-bin@npm:2.0.0" + checksum: 7c5379f9b188b564c4332c97bdd9a5d6b7b15f02b5823b00989d6a0e6fb31eb0280f02b0a924f930e1fcaf00e60fae333aec8923d2a4c7747613c7d629d8aa25 languageName: node linkType: hard @@ -22227,19 +22748,19 @@ __metadata: languageName: node linkType: hard -"npm-package-arg@npm:^10.0.0, npm-package-arg@npm:^10.1.0": - version: 10.1.0 - resolution: "npm-package-arg@npm:10.1.0" +"npm-package-arg@npm:^9.0.0, npm-package-arg@npm:^9.0.1": + version: 9.1.0 + resolution: "npm-package-arg@npm:9.1.0" dependencies: - hosted-git-info: ^6.0.0 - proc-log: ^3.0.0 + hosted-git-info: ^5.0.0 + proc-log: ^2.0.1 semver: ^7.3.5 - validate-npm-package-name: ^5.0.0 - checksum: 8fe4b6a742502345e4836ed42fdf26c544c9f75563c476c67044a481ada6e81f71b55462489c7e1899d516e4347150e58028036a90fa11d47e320bcc9365fd30 + validate-npm-package-name: ^4.0.0 + checksum: 277c21477731a4f1e31bde36f0db5f5470deb2a008db2aaf1b015d588b23cb225c75f90291ea241235e86682a03de972bbe69fc805c921a786ea9616955990b9 languageName: node linkType: hard -"npm-packlist@npm:5.1.1": +"npm-packlist@npm:^5.1.0, npm-packlist@npm:^5.1.1": version: 5.1.1 resolution: "npm-packlist@npm:5.1.1" dependencies: @@ -22253,39 +22774,30 @@ __metadata: languageName: node linkType: hard -"npm-packlist@npm:^7.0.0": - version: 7.0.4 - resolution: "npm-packlist@npm:7.0.4" +"npm-pick-manifest@npm:^7.0.0": + version: 7.0.1 + resolution: "npm-pick-manifest@npm:7.0.1" dependencies: - ignore-walk: ^6.0.0 - checksum: 5ffa1f8f0b32141a60a66713fa3ed03b8ee4800b1ed6b59194d03c3c85da88f3fc21e1de29b665f322678bae85198732b16aa76c0a7cb0e283f9e0db50752233 - languageName: node - linkType: hard - -"npm-pick-manifest@npm:^8.0.0": - version: 8.0.2 - resolution: "npm-pick-manifest@npm:8.0.2" - dependencies: - npm-install-checks: ^6.0.0 - npm-normalize-package-bin: ^3.0.0 - npm-package-arg: ^10.0.0 + npm-install-checks: ^5.0.0 + npm-normalize-package-bin: ^1.0.1 + npm-package-arg: ^9.0.0 semver: ^7.3.5 - checksum: c9f71b57351a3a241a7e56148332f2f341a09dff2a1b1f4ffb1517eac25f1888ac7fbce4939e522cbd533577448c307d05fff0c32430cc03c8c6179fac320cd4 + checksum: 9a4a8e64d2214783b2b74a361845000f5d91bb40c7858e2a30af2ac7876d9296efc37f8cacf60335e96a45effee2035b033d9bdefb4889757cc60d85959accbb languageName: node linkType: hard -"npm-registry-fetch@npm:^14.0.0, npm-registry-fetch@npm:^14.0.3, npm-registry-fetch@npm:^14.0.5": - version: 14.0.5 - resolution: "npm-registry-fetch@npm:14.0.5" +"npm-registry-fetch@npm:^13.0.0, npm-registry-fetch@npm:^13.0.1, npm-registry-fetch@npm:^13.3.0": + version: 13.3.1 + resolution: "npm-registry-fetch@npm:13.3.1" dependencies: - make-fetch-happen: ^11.0.0 - minipass: ^5.0.0 - minipass-fetch: ^3.0.0 + make-fetch-happen: ^10.0.6 + minipass: ^3.1.6 + minipass-fetch: ^2.0.3 minipass-json-stream: ^1.0.1 minizlib: ^2.1.2 - npm-package-arg: ^10.0.0 - proc-log: ^3.0.0 - checksum: c63649642955b424bc1baaff5955027144af312ae117ba8c24829e74484f859482591fe89687c6597d83e930c8054463eef23020ac69146097a72cc62ff10986 + npm-package-arg: ^9.0.1 + proc-log: ^2.0.0 + checksum: 5a941c2c799568e0dbccfc15f280444da398dadf2eede1b1921f08ddd5cb5f32c7cb4d16be96401f95a33073aeec13a3fd928c753790d3c412c2e64e7f7c6ee4 languageName: node linkType: hard @@ -22345,80 +22857,47 @@ __metadata: languageName: node linkType: hard -"nx@npm:16.10.0, nx@npm:>=16.5.1 < 17": - version: 16.10.0 - resolution: "nx@npm:16.10.0" +"nx@npm:14.8.2, nx@npm:>=14.6.1 < 16": + version: 14.8.2 + resolution: "nx@npm:14.8.2" dependencies: - "@nrwl/tao": 16.10.0 - "@nx/nx-darwin-arm64": 16.10.0 - "@nx/nx-darwin-x64": 16.10.0 - "@nx/nx-freebsd-x64": 16.10.0 - "@nx/nx-linux-arm-gnueabihf": 16.10.0 - "@nx/nx-linux-arm64-gnu": 16.10.0 - "@nx/nx-linux-arm64-musl": 16.10.0 - "@nx/nx-linux-x64-gnu": 16.10.0 - "@nx/nx-linux-x64-musl": 16.10.0 - "@nx/nx-win32-arm64-msvc": 16.10.0 - "@nx/nx-win32-x64-msvc": 16.10.0 + "@nrwl/cli": 14.8.2 + "@nrwl/tao": 14.8.2 "@parcel/watcher": 2.0.4 "@yarnpkg/lockfile": ^1.1.0 - "@yarnpkg/parsers": 3.0.0-rc.46 + "@yarnpkg/parsers": ^3.0.0-rc.18 "@zkochan/js-yaml": 0.0.6 - axios: ^1.0.0 - chalk: ^4.1.0 + chalk: 4.1.0 + chokidar: ^3.5.1 cli-cursor: 3.1.0 cli-spinners: 2.6.1 - cliui: ^8.0.1 - dotenv: ~16.3.1 - dotenv-expand: ~10.0.0 + cliui: ^7.0.2 + dotenv: ~10.0.0 enquirer: ~2.3.6 + fast-glob: 3.2.7 figures: 3.2.0 flat: ^5.0.2 - fs-extra: ^11.1.0 + fs-extra: ^10.1.0 glob: 7.1.4 ignore: ^5.0.4 - jest-diff: ^29.4.1 js-yaml: 4.1.0 jsonc-parser: 3.2.0 - lines-and-columns: ~2.0.3 minimatch: 3.0.5 - node-machine-id: 1.1.12 npm-run-path: ^4.0.1 open: ^8.4.0 - semver: 7.5.3 + semver: 7.3.4 string-width: ^4.2.3 strong-log-transformer: ^2.1.0 tar-stream: ~2.2.0 tmp: ~0.2.1 - tsconfig-paths: ^4.1.2 + tsconfig-paths: ^3.9.0 tslib: ^2.3.0 v8-compile-cache: 2.3.0 - yargs: ^17.6.2 - yargs-parser: 21.1.1 + yargs: ^17.4.0 + yargs-parser: 21.0.1 peerDependencies: - "@swc-node/register": ^1.6.7 - "@swc/core": ^1.3.85 - dependenciesMeta: - "@nx/nx-darwin-arm64": - optional: true - "@nx/nx-darwin-x64": - optional: true - "@nx/nx-freebsd-x64": - optional: true - "@nx/nx-linux-arm-gnueabihf": - optional: true - "@nx/nx-linux-arm64-gnu": - optional: true - "@nx/nx-linux-arm64-musl": - optional: true - "@nx/nx-linux-x64-gnu": - optional: true - "@nx/nx-linux-x64-musl": - optional: true - "@nx/nx-win32-arm64-msvc": - optional: true - "@nx/nx-win32-x64-msvc": - optional: true + "@swc-node/register": ^1.4.2 + "@swc/core": ^1.2.173 peerDependenciesMeta: "@swc-node/register": optional: true @@ -22426,7 +22905,7 @@ __metadata: optional: true bin: nx: bin/nx.js - checksum: 961b290f65dba76cf6cda62377930ac70fb5546d2992fde19ab028c7b4c37b76fc14eaa89f1d071b95e6d701932a2fd77678849172115045fd835ef9758e93bb + checksum: b0c0428366f867e20d5f89d8e9bf2f8c8b6f9c0a60a7b8bebc3617d652b0e33109bc8bce352b9e7218db69eb181b0bacb3378c3d0f5b063acfd986ed0b35f7df languageName: node linkType: hard @@ -22801,14 +23280,14 @@ __metadata: languageName: node linkType: hard -"p-map-series@npm:2.1.0": +"p-map-series@npm:^2.1.0": version: 2.1.0 resolution: "p-map-series@npm:2.1.0" checksum: 69d4efbb6951c0dd62591d5a18c3af0af78496eae8b55791e049da239d70011aa3af727dece3fc9943e0bb3fd4fa64d24177cfbecc46efaf193179f0feeac486 languageName: node linkType: hard -"p-map@npm:4.0.0, p-map@npm:^4.0.0": +"p-map@npm:^4.0.0": version: 4.0.0 resolution: "p-map@npm:4.0.0" dependencies: @@ -22817,14 +23296,14 @@ __metadata: languageName: node linkType: hard -"p-pipe@npm:3.1.0": +"p-pipe@npm:^3.1.0": version: 3.1.0 resolution: "p-pipe@npm:3.1.0" checksum: ee9a2609685f742c6ceb3122281ec4453bbbcc80179b13e66fd139dcf19b1c327cf6c2fdfc815b548d6667e7eaefe5396323f6d49c4f7933e4cef47939e3d65c languageName: node linkType: hard -"p-queue@npm:6.6.2": +"p-queue@npm:^6.6.2": version: 6.6.2 resolution: "p-queue@npm:6.6.2" dependencies: @@ -22834,7 +23313,7 @@ __metadata: languageName: node linkType: hard -"p-reduce@npm:2.1.0, p-reduce@npm:^2.0.0, p-reduce@npm:^2.1.0": +"p-reduce@npm:^2.0.0, p-reduce@npm:^2.1.0": version: 2.1.0 resolution: "p-reduce@npm:2.1.0" checksum: 99b26d36066a921982f25c575e78355824da0787c486e3dd9fc867460e8bf17d5fb3ce98d006b41bdc81ffc0aa99edf5faee53d11fe282a20291fb721b0cb1c7 @@ -22874,7 +23353,7 @@ __metadata: languageName: node linkType: hard -"p-waterfall@npm:2.1.1": +"p-waterfall@npm:^2.1.1": version: 2.1.1 resolution: "p-waterfall@npm:2.1.1" dependencies: @@ -22883,31 +23362,34 @@ __metadata: languageName: node linkType: hard -"pacote@npm:^15.2.0": - version: 15.2.0 - resolution: "pacote@npm:15.2.0" +"pacote@npm:^13.0.3, pacote@npm:^13.6.1": + version: 13.6.1 + resolution: "pacote@npm:13.6.1" dependencies: - "@npmcli/git": ^4.0.0 - "@npmcli/installed-package-contents": ^2.0.1 - "@npmcli/promise-spawn": ^6.0.1 - "@npmcli/run-script": ^6.0.0 - cacache: ^17.0.0 - fs-minipass: ^3.0.0 - minipass: ^5.0.0 - npm-package-arg: ^10.0.0 - npm-packlist: ^7.0.0 - npm-pick-manifest: ^8.0.0 - npm-registry-fetch: ^14.0.0 - proc-log: ^3.0.0 + "@npmcli/git": ^3.0.0 + "@npmcli/installed-package-contents": ^1.0.7 + "@npmcli/promise-spawn": ^3.0.0 + "@npmcli/run-script": ^4.1.0 + cacache: ^16.0.0 + chownr: ^2.0.0 + fs-minipass: ^2.1.0 + infer-owner: ^1.0.4 + minipass: ^3.1.6 + mkdirp: ^1.0.4 + npm-package-arg: ^9.0.0 + npm-packlist: ^5.1.0 + npm-pick-manifest: ^7.0.0 + npm-registry-fetch: ^13.0.1 + proc-log: ^2.0.0 promise-retry: ^2.0.1 - read-package-json: ^6.0.0 - read-package-json-fast: ^3.0.0 - sigstore: ^1.3.0 - ssri: ^10.0.0 + read-package-json: ^5.0.0 + read-package-json-fast: ^2.0.3 + rimraf: ^3.0.2 + ssri: ^9.0.0 tar: ^6.1.11 bin: pacote: lib/bin.js - checksum: c731572be2bf226b117eba076d242bd4cd8be7aa01e004af3374a304ad7ab330539e22644bc33de12d2a7d45228ccbcbf4d710f59c84414f3d09a1a95ee6f0bf + checksum: 26cebb59aea93d03ad051d82c4f2300beb333ded0f16ba92cfe976b5600157bd1ee034afe1c86406bbe5eacd51d413797939b08aa58adcf73f7680aead9e667f languageName: node linkType: hard @@ -22951,6 +23433,17 @@ __metadata: languageName: node linkType: hard +"parse-conflict-json@npm:^2.0.1": + version: 2.0.2 + resolution: "parse-conflict-json@npm:2.0.2" + dependencies: + json-parse-even-better-errors: ^2.3.1 + just-diff: ^5.0.1 + just-diff-apply: ^5.2.0 + checksum: 076f65c958696586daefb153f59d575dfb59648be43116a21b74d5ff69ec63dd56f585a27cc2da56d8e64ca5abf0373d6619b8330c035131f8d1e990c8406378 + languageName: node + linkType: hard + "parse-entities@npm:^2.0.0": version: 2.0.0 resolution: "parse-entities@npm:2.0.0" @@ -23111,7 +23604,7 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^1.10.1, path-scurry@npm:^1.6.1": +"path-scurry@npm:^1.10.1": version: 1.10.1 resolution: "path-scurry@npm:1.10.1" dependencies: @@ -23218,13 +23711,6 @@ __metadata: languageName: node linkType: hard -"pify@npm:5.0.0": - version: 5.0.0 - resolution: "pify@npm:5.0.0" - checksum: 443e3e198ad6bfa8c0c533764cf75c9d5bc976387a163792fb553ffe6ce923887cf14eebf5aea9b7caa8eab930da8c33612990ae85bd8c2bc18bedb9eae94ecb - languageName: node - linkType: hard - "pify@npm:^2.2.0, pify@npm:^2.3.0": version: 2.3.0 resolution: "pify@npm:2.3.0" @@ -23246,6 +23732,13 @@ __metadata: languageName: node linkType: hard +"pify@npm:^5.0.0": + version: 5.0.0 + resolution: "pify@npm:5.0.0" + checksum: 443e3e198ad6bfa8c0c533764cf75c9d5bc976387a163792fb553ffe6ce923887cf14eebf5aea9b7caa8eab930da8c33612990ae85bd8c2bc18bedb9eae94ecb + languageName: node + linkType: hard + "pirates@npm:^4.0.4, pirates@npm:^4.0.5": version: 4.0.5 resolution: "pirates@npm:4.0.5" @@ -23885,10 +24378,10 @@ __metadata: languageName: node linkType: hard -"proc-log@npm:^3.0.0": - version: 3.0.0 - resolution: "proc-log@npm:3.0.0" - checksum: 02b64e1b3919e63df06f836b98d3af002b5cd92655cab18b5746e37374bfb73e03b84fe305454614b34c25b485cc687a9eebdccf0242cda8fda2475dd2c97e02 +"proc-log@npm:^2.0.0, proc-log@npm:^2.0.1": + version: 2.0.1 + resolution: "proc-log@npm:2.0.1" + checksum: f6f23564ff759097db37443e6e2765af84979a703d2c52c1b9df506ee9f87caa101ba49d8fdc115c1a313ec78e37e8134704e9069e6a870f3499d98bb24c436f languageName: node linkType: hard @@ -23913,6 +24406,20 @@ __metadata: languageName: node linkType: hard +"promise-all-reject-late@npm:^1.0.0": + version: 1.0.1 + resolution: "promise-all-reject-late@npm:1.0.1" + checksum: d7d61ac412352e2c8c3463caa5b1c3ca0f0cc3db15a09f180a3da1446e33d544c4261fc716f772b95e4c27d559cfd2388540f44104feb356584f9c73cfb9ffcb + languageName: node + linkType: hard + +"promise-call-limit@npm:^1.0.1": + version: 1.0.1 + resolution: "promise-call-limit@npm:1.0.1" + checksum: e69aed17f5f34bbd7aecff28faedb456e3500a08af31ee759ef75f2d8c2219d7c0e59f153f4d8c339056de8c304e0dd4acc500c339e7ea1e9c0e7bb1444367c8 + languageName: node + linkType: hard + "promise-inflight@npm:^1.0.1": version: 1.0.1 resolution: "promise-inflight@npm:1.0.1" @@ -23956,12 +24463,12 @@ __metadata: languageName: node linkType: hard -"promzard@npm:^1.0.0": - version: 1.0.0 - resolution: "promzard@npm:1.0.0" +"promzard@npm:^0.3.0": + version: 0.3.0 + resolution: "promzard@npm:0.3.0" dependencies: - read: ^2.0.0 - checksum: c06948827171612faae321ebaf23ff8bd9ebb3e1e0f37616990bc4b81c663b192e447b3fe3b424211beb0062cec0cfe6ba3ce70c8b448b4aa59752b765dbb302 + read: 1 + checksum: 443a3b39ac916099988ee0161ab4e22edd1fa27e3d39a38d60e48c11ca6df3f5a90bfe44d95af06ed8659c4050b789ffe64c3f9f8e49a4bea1ea19105c98445a languageName: node linkType: hard @@ -23994,6 +24501,13 @@ __metadata: languageName: node linkType: hard +"proto-list@npm:~1.2.1": + version: 1.2.4 + resolution: "proto-list@npm:1.2.4" + checksum: 4d4826e1713cbfa0f15124ab0ae494c91b597a3c458670c9714c36e8baddf5a6aad22842776f2f5b137f259c8533e741771445eb8df82e861eea37a6eaba03f7 + languageName: node + linkType: hard + "protobufjs@npm:^7.2.4": version: 7.2.4 resolution: "protobufjs@npm:7.2.4" @@ -24045,7 +24559,7 @@ __metadata: languageName: node linkType: hard -"proxy-from-env@npm:^1.0.0, proxy-from-env@npm:^1.1.0": +"proxy-from-env@npm:^1.0.0": version: 1.1.0 resolution: "proxy-from-env@npm:1.1.0" checksum: ed7fcc2ba0a33404958e34d95d18638249a68c430e30fcb6c478497d72739ba64ce9810a24f53a7d921d0c065e5b78e3822759800698167256b04659366ca4d4 @@ -24143,6 +24657,13 @@ __metadata: languageName: node linkType: hard +"q@npm:^1.5.1": + version: 1.5.1 + resolution: "q@npm:1.5.1" + checksum: 147baa93c805bc1200ed698bdf9c72e9e42c05f96d007e33a558b5fdfd63e5ea130e99313f28efc1783e90e6bdb4e48b67a36fcc026b7b09202437ae88a1fb12 + languageName: node + linkType: hard + "qs@npm:6.10.4, qs@npm:~6.10.3": version: 6.10.4 resolution: "qs@npm:6.10.4" @@ -25463,32 +25984,32 @@ __metadata: languageName: node linkType: hard -"read-cmd-shim@npm:4.0.0": - version: 4.0.0 - resolution: "read-cmd-shim@npm:4.0.0" - checksum: 2fb5a8a38984088476f559b17c6a73324a5db4e77e210ae0aab6270480fd85c355fc990d1c79102e25e555a8201606ed12844d6e3cd9f35d6a1518791184e05b +"read-cmd-shim@npm:^3.0.0": + version: 3.0.0 + resolution: "read-cmd-shim@npm:3.0.0" + checksum: b518c6026f3320e30b692044f6ff5c4dc80f9c71261296da8994101b569b26b12b8e5df397bba2d4691dd3a3a2f770a1eca7be18a69ec202fac6dcfadc5016fd languageName: node linkType: hard -"read-package-json-fast@npm:^3.0.0": - version: 3.0.2 - resolution: "read-package-json-fast@npm:3.0.2" +"read-package-json-fast@npm:^2.0.2, read-package-json-fast@npm:^2.0.3": + version: 2.0.3 + resolution: "read-package-json-fast@npm:2.0.3" dependencies: - json-parse-even-better-errors: ^3.0.0 - npm-normalize-package-bin: ^3.0.0 - checksum: 8d406869f045f1d76e2a99865a8fd1c1af9c1dc06200b94d2b07eef87ed734b22703a8d72e1cd36ea36cc48e22020bdd187f88243c7dd0563f72114d38c17072 + json-parse-even-better-errors: ^2.3.0 + npm-normalize-package-bin: ^1.0.1 + checksum: fca37b3b2160b9dda7c5588b767f6a2b8ce68d03a044000e568208e20bea0cf6dd2de17b90740ce8da8b42ea79c0b3859649dadf29510bbe77224ea65326a903 languageName: node linkType: hard -"read-package-json@npm:6.0.4, read-package-json@npm:^6.0.0": - version: 6.0.4 - resolution: "read-package-json@npm:6.0.4" +"read-package-json@npm:^5.0.0, read-package-json@npm:^5.0.1": + version: 5.0.2 + resolution: "read-package-json@npm:5.0.2" dependencies: - glob: ^10.2.2 - json-parse-even-better-errors: ^3.0.0 - normalize-package-data: ^5.0.0 - npm-normalize-package-bin: ^3.0.0 - checksum: ce40c4671299753f1349aebe44693cd250d6936c4bacfb31cd884c87f24a0174ba5f651ee2866cf5e57365451cba38bc1db9c2a371e4ba7502fb46dcad50f1d7 + glob: ^8.0.1 + json-parse-even-better-errors: ^2.3.1 + normalize-package-data: ^4.0.0 + npm-normalize-package-bin: ^2.0.0 + checksum: 0882ac9cec1bc92fb5515e9727611fb2909351e1e5c840dce3503cbb25b4cd48eb44b61071986e0fc51043208161f07d364a7336206c8609770186818753b51a languageName: node linkType: hard @@ -25559,12 +26080,12 @@ __metadata: languageName: node linkType: hard -"read@npm:^2.0.0": - version: 2.1.0 - resolution: "read@npm:2.1.0" +"read@npm:1, read@npm:^1.0.7": + version: 1.0.7 + resolution: "read@npm:1.0.7" dependencies: - mute-stream: ~1.0.0 - checksum: e745999138022b56d32daf7cce9b7552b2ec648e4e2578d076a410575a0a400faf74f633dd74ef1b1c42563397d322c1ad5a0068471c38978b02ef97056c2991 + mute-stream: ~0.0.4 + checksum: 2777c254e5732cac96f5d0a1c0f6b836c89ae23d8febd405b206f6f24d5de1873420f1a0795e0e3721066650d19adf802c7882c4027143ee0acf942a4f34f97b languageName: node linkType: hard @@ -25606,6 +26127,18 @@ __metadata: languageName: node linkType: hard +"readdir-scoped-modules@npm:^1.1.0": + version: 1.1.0 + resolution: "readdir-scoped-modules@npm:1.1.0" + dependencies: + debuglog: ^1.0.1 + dezalgo: ^1.0.0 + graceful-fs: ^4.1.2 + once: ^1.3.0 + checksum: 6d9f334e40dfd0f5e4a8aab5e67eb460c95c85083c690431f87ab2c9135191170e70c2db6d71afcafb78e073d23eb95dcb3fc33ef91308f6ebfe3197be35e608 + languageName: node + linkType: hard + "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -25993,13 +26526,6 @@ __metadata: languageName: node linkType: hard -"resolve-from@npm:5.0.0, resolve-from@npm:^5.0.0": - version: 5.0.0 - resolution: "resolve-from@npm:5.0.0" - checksum: 4ceeb9113e1b1372d0cd969f3468fa042daa1dd9527b1b6bb88acb6ab55d8b9cd65dbf18819f9f9ddf0db804990901dcdaade80a215e7b2c23daae38e64f5bdf - languageName: node - linkType: hard - "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -26007,6 +26533,13 @@ __metadata: languageName: node linkType: hard +"resolve-from@npm:^5.0.0": + version: 5.0.0 + resolution: "resolve-from@npm:5.0.0" + checksum: 4ceeb9113e1b1372d0cd969f3468fa042daa1dd9527b1b6bb88acb6ab55d8b9cd65dbf18819f9f9ddf0db804990901dcdaade80a215e7b2c23daae38e64f5bdf + languageName: node + linkType: hard + "resolve-options@npm:^1.1.0": version: 1.1.0 resolution: "resolve-options@npm:1.1.0" @@ -26176,17 +26709,6 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:^4.4.1": - version: 4.4.1 - resolution: "rimraf@npm:4.4.1" - dependencies: - glob: ^9.2.0 - bin: - rimraf: dist/cjs/src/bin.js - checksum: b786adc02651e2e24bbedb04bbdea80652fc9612632931ff2d9f898c5e4708fe30956186597373c568bd5230a4dc2fadfc816ccacba8a1daded3a006a6b74f1a - languageName: node - linkType: hard - "rimraf@npm:~2.6.2": version: 2.6.3 resolution: "rimraf@npm:2.6.3" @@ -26558,17 +27080,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.5.3": - version: 7.5.3 - resolution: "semver@npm:7.5.3" - dependencies: - lru-cache: ^6.0.0 - bin: - semver: bin/semver.js - checksum: 9d58db16525e9f749ad0a696a1f27deabaa51f66e91d2fa2b0db3de3e9644e8677de3b7d7a03f4c15bc81521e0c3916d7369e0572dbde250d9bedf5194e2a8a7 - languageName: node - linkType: hard - "semver@npm:7.5.4, semver@npm:7.x, 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.1, semver@npm:^7.5.3, semver@npm:^7.5.4": version: 7.5.4 resolution: "semver@npm:7.5.4" @@ -26780,7 +27291,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:3.0.7, signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": +"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 @@ -26794,21 +27305,6 @@ __metadata: languageName: node linkType: hard -"sigstore@npm:^1.3.0, sigstore@npm:^1.4.0": - version: 1.9.0 - resolution: "sigstore@npm:1.9.0" - dependencies: - "@sigstore/bundle": ^1.1.0 - "@sigstore/protobuf-specs": ^0.2.0 - "@sigstore/sign": ^1.0.0 - "@sigstore/tuf": ^1.0.3 - make-fetch-happen: ^11.0.1 - bin: - sigstore: bin/sigstore.js - checksum: b3f1ccf4d2d5e6af294ad851981cc9dc4c01b6b5b7aeb98582765f5d2e75aa2b9221133b8e572179bb305e16ce589339d9617b26b9fa0bea0c38c9adef792912 - languageName: node - linkType: hard - "simple-git@npm:^3.6.0": version: 3.16.0 resolution: "simple-git@npm:3.16.0" @@ -26854,7 +27350,7 @@ __metadata: languageName: node linkType: hard -"slash@npm:3.0.0, slash@npm:^3.0.0": +"slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" checksum: 94a93fff615f25a999ad4b83c9d5e257a7280c90a32a7cb8b4a87996e4babf322e469c42b7f649fd5796edd8687652f3fb452a86dc97a816f01113183393f11c @@ -27080,6 +27576,15 @@ __metadata: languageName: node linkType: hard +"sort-keys@npm:^4.0.0": + version: 4.2.0 + resolution: "sort-keys@npm:4.2.0" + dependencies: + is-plain-obj: ^2.0.0 + checksum: 1535ffd5a789259fc55107d5c3cec09b3e47803a9407fcaae37e1b9e0b813762c47dfee35b6e71e20ca7a69798d0a4791b2058a07f6cab5ef17b2dae83cedbda + languageName: node + linkType: hard + "sort-keys@npm:^5.0.0": version: 5.0.0 resolution: "sort-keys@npm:5.0.0" @@ -27253,7 +27758,7 @@ __metadata: languageName: node linkType: hard -"split2@npm:^3.2.2": +"split2@npm:^3.0.0": version: 3.2.2 resolution: "split2@npm:3.2.2" dependencies: @@ -27271,7 +27776,7 @@ __metadata: languageName: node linkType: hard -"split@npm:^1.0.1": +"split@npm:^1.0.0": version: 1.0.1 resolution: "split@npm:1.0.1" dependencies: @@ -27332,15 +27837,6 @@ __metadata: languageName: node linkType: hard -"ssri@npm:^10.0.0, ssri@npm:^10.0.1": - version: 10.0.5 - resolution: "ssri@npm:10.0.5" - dependencies: - minipass: ^7.0.3 - checksum: 0a31b65f21872dea1ed3f7c200d7bc1c1b91c15e419deca14f282508ba917cbb342c08a6814c7f68ca4ca4116dd1a85da2bbf39227480e50125a1ceffeecb750 - languageName: node - linkType: hard - "ssri@npm:^8.0.0, ssri@npm:^8.0.1": version: 8.0.1 resolution: "ssri@npm:8.0.1" @@ -27736,7 +28232,7 @@ __metadata: languageName: node linkType: hard -"strong-log-transformer@npm:2.1.0, strong-log-transformer@npm:^2.1.0": +"strong-log-transformer@npm:^2.1.0": version: 2.1.0 resolution: "strong-log-transformer@npm:2.1.0" dependencies: @@ -28051,21 +28547,7 @@ __metadata: languageName: node linkType: hard -"tar@npm:6.1.11": - version: 6.1.11 - resolution: "tar@npm:6.1.11" - dependencies: - chownr: ^2.0.0 - fs-minipass: ^2.0.0 - minipass: ^3.0.0 - minizlib: ^2.1.1 - mkdirp: ^1.0.3 - yallist: ^4.0.0 - checksum: a04c07bb9e2d8f46776517d4618f2406fb977a74d914ad98b264fc3db0fe8224da5bec11e5f8902c5b9bcb8ace22d95fbe3c7b36b8593b7dfc8391a25898f32f - languageName: node - linkType: hard - -"tar@npm:^6.0.2, tar@npm:^6.1.11, tar@npm:^6.1.13, tar@npm:^6.1.2": +"tar@npm:^6.0.2, tar@npm:^6.1.0, tar@npm:^6.1.11, tar@npm:^6.1.13, tar@npm:^6.1.2": version: 6.1.13 resolution: "tar@npm:6.1.13" dependencies: @@ -28097,7 +28579,7 @@ __metadata: languageName: node linkType: hard -"temp-dir@npm:1.0.0": +"temp-dir@npm:^1.0.0": version: 1.0.0 resolution: "temp-dir@npm:1.0.0" checksum: cb2b58ddfb12efa83e939091386ad73b425c9a8487ea0095fe4653192a40d49184a771a1beba99045fbd011e389fd563122d79f54f82be86a55620667e08a6b2 @@ -28275,7 +28757,7 @@ __metadata: languageName: node linkType: hard -"through2@npm:~4.0.2": +"through2@npm:^4.0.0, through2@npm:~4.0.2": version: 4.0.2 resolution: "through2@npm:4.0.2" dependencies: @@ -28491,6 +28973,13 @@ __metadata: languageName: node linkType: hard +"treeverse@npm:^2.0.0": + version: 2.0.0 + resolution: "treeverse@npm:2.0.0" + checksum: 3c6b2b890975a4d42c86b9a0f1eb932b4450db3fa874be5c301c4f5e306fd76330c6a490cf334b0937b3a44b049787ba5d98c88bc7b140f34fdb3ab1f83e5269 + languageName: node + linkType: hard + "trim-newlines@npm:^3.0.0": version: 3.0.1 resolution: "trim-newlines@npm:3.0.1" @@ -28648,7 +29137,7 @@ __metadata: languageName: node linkType: hard -"tsconfig-paths@npm:^3.14.2": +"tsconfig-paths@npm:^3.14.2, tsconfig-paths@npm:^3.9.0": version: 3.14.2 resolution: "tsconfig-paths@npm:3.14.2" dependencies: @@ -28660,17 +29149,6 @@ __metadata: languageName: node linkType: hard -"tsconfig-paths@npm:^4.1.2": - version: 4.2.0 - resolution: "tsconfig-paths@npm:4.2.0" - dependencies: - json5: ^2.2.2 - minimist: ^1.2.6 - strip-bom: ^3.0.0 - checksum: 28c5f7bbbcabc9dabd4117e8fdc61483f6872a1c6b02a4b1c4d68c5b79d06896c3cc9547610c4c3ba64658531caa2de13ead1ea1bf321c7b53e969c4752b98c7 - languageName: node - linkType: hard - "tslib@npm:2.4.0": version: 2.4.0 resolution: "tslib@npm:2.4.0" @@ -28710,17 +29188,6 @@ __metadata: languageName: node linkType: hard -"tuf-js@npm:^1.1.7": - version: 1.1.7 - resolution: "tuf-js@npm:1.1.7" - dependencies: - "@tufjs/models": 1.0.4 - debug: ^4.3.4 - make-fetch-happen: ^11.1.1 - checksum: 089fc0dabe1fcaeca8b955b358b34272f23237ac9e074b5f983349eb44d9688fd137f28f493bbd8dfd865d1af4e76e0cc869d307eadd054d1b404914c3124ae5 - languageName: node - linkType: hard - "tunnel-agent@npm:^0.6.0": version: 0.6.0 resolution: "tunnel-agent@npm:0.6.0" @@ -28887,6 +29354,15 @@ __metadata: languageName: node linkType: hard +"typedarray-to-buffer@npm:^3.1.5": + version: 3.1.5 + resolution: "typedarray-to-buffer@npm:3.1.5" + dependencies: + is-typedarray: ^1.0.0 + checksum: 99c11aaa8f45189fcfba6b8a4825fd684a321caa9bd7a76a27cf0c7732c174d198b99f449c52c3818107430b5f41c0ccbbfb75cb2ee3ca4a9451710986d61a60 + languageName: node + linkType: hard + "typedarray@npm:^0.0.6": version: 0.0.6 resolution: "typedarray@npm:0.0.6" @@ -28894,7 +29370,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:4.8.4, typescript@npm:^4.2.4": +"typescript@npm:4.8.4, typescript@npm:>=2.7, typescript@npm:^3 || ^4, typescript@npm:^4.2.4": version: 4.8.4 resolution: "typescript@npm:4.8.4" bin: @@ -28904,17 +29380,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:>=2.7, typescript@npm:>=3 < 6": - version: 5.2.2 - resolution: "typescript@npm:5.2.2" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 7912821dac4d962d315c36800fe387cdc0a6298dba7ec171b350b4a6e988b51d7b8f051317786db1094bd7431d526b648aba7da8236607febb26cf5b871d2d3c - languageName: node - linkType: hard - -"typescript@patch:typescript@4.8.4#~builtin, typescript@patch:typescript@^4.2.4#~builtin": +"typescript@patch:typescript@4.8.4#~builtin, typescript@patch:typescript@>=2.7#~builtin, typescript@patch:typescript@^3 || ^4#~builtin, typescript@patch:typescript@^4.2.4#~builtin": version: 4.8.4 resolution: "typescript@patch:typescript@npm%3A4.8.4#~builtin::version=4.8.4&hash=1a91c8" bin: @@ -28924,16 +29390,6 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@>=2.7#~builtin, typescript@patch:typescript@>=3 < 6#~builtin": - version: 5.2.2 - resolution: "typescript@patch:typescript@npm%3A5.2.2#~builtin::version=5.2.2&hash=14eedb" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 07106822b4305de3f22835cbba949a2b35451cad50888759b6818421290ff95d522b38ef7919e70fb381c5fe9c1c643d7dea22c8b31652a717ddbd57b7f4d554 - languageName: node - linkType: hard - "ua-parser-js@npm:^1.0.32": version: 1.0.33 resolution: "ua-parser-js@npm:1.0.33" @@ -29035,15 +29491,6 @@ __metadata: languageName: node linkType: hard -"unique-filename@npm:^3.0.0": - version: 3.0.0 - resolution: "unique-filename@npm:3.0.0" - dependencies: - unique-slug: ^4.0.0 - checksum: 8e2f59b356cb2e54aab14ff98a51ac6c45781d15ceaab6d4f1c2228b780193dc70fae4463ce9e1df4479cb9d3304d7c2043a3fb905bdeca71cc7e8ce27e063df - languageName: node - linkType: hard - "unique-slug@npm:^2.0.0": version: 2.0.2 resolution: "unique-slug@npm:2.0.2" @@ -29053,15 +29500,6 @@ __metadata: languageName: node linkType: hard -"unique-slug@npm:^4.0.0": - version: 4.0.0 - resolution: "unique-slug@npm:4.0.0" - dependencies: - imurmurhash: ^0.1.4 - checksum: 0884b58365af59f89739e6f71e3feacb5b1b41f2df2d842d0757933620e6de08eff347d27e9d499b43c40476cbaf7988638d3acb2ffbcb9d35fd035591adfd15 - languageName: node - linkType: hard - "unique-stream@npm:^2.0.2": version: 2.3.1 resolution: "unique-stream@npm:2.3.1" @@ -29163,7 +29601,7 @@ __metadata: languageName: node linkType: hard -"upath@npm:2.0.1": +"upath@npm:^2.0.1": version: 2.0.1 resolution: "upath@npm:2.0.1" checksum: 2db04f24a03ef72204c7b969d6991abec9e2cb06fb4c13a1fd1c59bc33b46526b16c3325e55930a11ff86a77a8cbbcda8f6399bf914087028c5beae21ecdb33c @@ -29395,7 +29833,7 @@ __metadata: languageName: node linkType: hard -"validate-npm-package-license@npm:3.0.4, validate-npm-package-license@npm:^3.0.1, validate-npm-package-license@npm:^3.0.4": +"validate-npm-package-license@npm:^3.0.1, validate-npm-package-license@npm:^3.0.4": version: 3.0.4 resolution: "validate-npm-package-license@npm:3.0.4" dependencies: @@ -29405,15 +29843,6 @@ __metadata: languageName: node linkType: hard -"validate-npm-package-name@npm:5.0.0, validate-npm-package-name@npm:^5.0.0": - version: 5.0.0 - resolution: "validate-npm-package-name@npm:5.0.0" - dependencies: - builtins: ^5.0.0 - checksum: 5342a994986199b3c28e53a8452a14b2bb5085727691ea7aa0d284a6606b127c371e0925ae99b3f1ef7cc7d2c9de75f52eb61a3d1cc45e39bca1e3a9444cbb4e - languageName: node - linkType: hard - "validate-npm-package-name@npm:^3.0.0": version: 3.0.0 resolution: "validate-npm-package-name@npm:3.0.0" @@ -29423,6 +29852,15 @@ __metadata: languageName: node linkType: hard +"validate-npm-package-name@npm:^4.0.0": + version: 4.0.0 + resolution: "validate-npm-package-name@npm:4.0.0" + dependencies: + builtins: ^5.0.0 + checksum: a32fd537bad17fcb59cfd58ae95a414d443866020d448ec3b22e8d40550cb585026582a57efbe1f132b882eea4da8ac38ee35f7be0dd72988a3cb55d305a20c1 + languageName: node + linkType: hard + "value-equal@npm:^1.0.1": version: 1.0.1 resolution: "value-equal@npm:1.0.1" @@ -29602,6 +30040,13 @@ __metadata: languageName: node linkType: hard +"walk-up-path@npm:^1.0.0": + version: 1.0.0 + resolution: "walk-up-path@npm:1.0.0" + checksum: b8019ac4fb9ba1576839ec66d2217f62ab773c1cc4c704bfd1c79b1359fef5366f1382d3ab230a66a14c3adb1bf0fe102d1fdaa3437881e69154dfd1432abd32 + languageName: node + linkType: hard + "walker@npm:^1.0.8": version: 1.0.8 resolution: "walker@npm:1.0.8" @@ -30104,17 +30549,6 @@ __metadata: languageName: node linkType: hard -"which@npm:^3.0.0": - version: 3.0.1 - resolution: "which@npm:3.0.1" - dependencies: - isexe: ^2.0.0 - bin: - node-which: bin/which.js - checksum: adf720fe9d84be2d9190458194f814b5e9015ae4b88711b150f30d0f4d0b646544794b86f02c7ebeec1db2029bc3e83a7ff156f542d7521447e5496543e26890 - languageName: node - linkType: hard - "wide-align@npm:^1.1.0, wide-align@npm:^1.1.5": version: 1.1.5 resolution: "wide-align@npm:1.1.5" @@ -30185,16 +30619,6 @@ __metadata: languageName: node linkType: hard -"write-file-atomic@npm:5.0.1, write-file-atomic@npm:^5.0.1": - version: 5.0.1 - resolution: "write-file-atomic@npm:5.0.1" - dependencies: - imurmurhash: ^0.1.4 - signal-exit: ^4.0.1 - checksum: 8dbb0e2512c2f72ccc20ccedab9986c7d02d04039ed6e8780c987dc4940b793339c50172a1008eed7747001bfacc0ca47562668a069a7506c46c77d7ba3926a9 - languageName: node - linkType: hard - "write-file-atomic@npm:^2.3.0, write-file-atomic@npm:^2.4.2": version: 2.4.3 resolution: "write-file-atomic@npm:2.4.3" @@ -30206,7 +30630,19 @@ __metadata: languageName: node linkType: hard -"write-file-atomic@npm:^4.0.2": +"write-file-atomic@npm:^3.0.0": + version: 3.0.3 + resolution: "write-file-atomic@npm:3.0.3" + dependencies: + imurmurhash: ^0.1.4 + is-typedarray: ^1.0.0 + signal-exit: ^3.0.2 + typedarray-to-buffer: ^3.1.5 + checksum: c55b24617cc61c3a4379f425fc62a386cc51916a9b9d993f39734d005a09d5a4bb748bc251f1304e7abd71d0a26d339996c275955f527a131b1dcded67878280 + languageName: node + linkType: hard + +"write-file-atomic@npm:^4.0.0, write-file-atomic@npm:^4.0.1, write-file-atomic@npm:^4.0.2": version: 4.0.2 resolution: "write-file-atomic@npm:4.0.2" dependencies: @@ -30216,6 +30652,16 @@ __metadata: languageName: node linkType: hard +"write-file-atomic@npm:^5.0.1": + version: 5.0.1 + resolution: "write-file-atomic@npm:5.0.1" + dependencies: + imurmurhash: ^0.1.4 + signal-exit: ^4.0.1 + checksum: 8dbb0e2512c2f72ccc20ccedab9986c7d02d04039ed6e8780c987dc4940b793339c50172a1008eed7747001bfacc0ca47562668a069a7506c46c77d7ba3926a9 + languageName: node + linkType: hard + "write-json-file@npm:^3.2.0": version: 3.2.0 resolution: "write-json-file@npm:3.2.0" @@ -30230,7 +30676,21 @@ __metadata: languageName: node linkType: hard -"write-pkg@npm:4.0.0": +"write-json-file@npm:^4.3.0": + version: 4.3.0 + resolution: "write-json-file@npm:4.3.0" + dependencies: + detect-indent: ^6.0.0 + graceful-fs: ^4.1.15 + is-plain-obj: ^2.0.0 + make-dir: ^3.0.0 + sort-keys: ^4.0.0 + write-file-atomic: ^3.0.0 + checksum: 33908c591923dc273e6574e7c0e2df157acfcf498e3a87c5615ced006a465c4058877df6abce6fc1acd2844fa3cf4518ace4a34d5d82ab28bcf896317ba1db6f + languageName: node + linkType: hard + +"write-pkg@npm:^4.0.0": version: 4.0.0 resolution: "write-pkg@npm:4.0.0" dependencies: @@ -30394,10 +30854,10 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:21.1.1, yargs-parser@npm:^21.0.1, yargs-parser@npm:^21.1.1": - version: 21.1.1 - resolution: "yargs-parser@npm:21.1.1" - checksum: ed2d96a616a9e3e1cc7d204c62ecc61f7aaab633dcbfab2c6df50f7f87b393993fe6640d017759fe112d0cb1e0119f2b4150a87305cc873fd90831c6a58ccf1c +"yargs-parser@npm:21.0.1": + version: 21.0.1 + resolution: "yargs-parser@npm:21.0.1" + checksum: c3ea2ed12cad0377ce3096b3f138df8267edf7b1aa7d710cd502fe16af417bafe4443dd71b28158c22fcd1be5dfd0e86319597e47badf42ff83815485887323a languageName: node linkType: hard @@ -30408,6 +30868,13 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^21.0.1, yargs-parser@npm:^21.1.1": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: ed2d96a616a9e3e1cc7d204c62ecc61f7aaab633dcbfab2c6df50f7f87b393993fe6640d017759fe112d0cb1e0119f2b4150a87305cc873fd90831c6a58ccf1c + languageName: node + linkType: hard + "yargs-unparser@npm:2.0.0": version: 2.0.0 resolution: "yargs-unparser@npm:2.0.0" @@ -30435,7 +30902,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2": +"yargs@npm:^17.3.1, yargs@npm:^17.4.0, yargs@npm:^17.5.1": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: From 33f959612f4baed3ffceab15001e8f2fd1dd9266 Mon Sep 17 00:00:00 2001 From: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> Date: Tue, 24 Oct 2023 07:11:17 -0700 Subject: [PATCH 027/180] Docs: Clarify content on Getting started page (#77010) * Clarify content Users coming to this page need to understand that they have landed on the OSS page, and be provided direction to the Cloud getting started if they have landed here by mistake. * Update link update link * Remove duplicate content * Add link to cloud docs * Update docs/sources/getting-started/_index.md try this * Update docs/sources/getting-started/_index.md * Update docs/sources/getting-started/_index.md * Update _index.md * Fixed linting issues --------- Co-authored-by: Eve Meelan <81647476+Eve832@users.noreply.github.com> Co-authored-by: Isabel Matwawana --- docs/sources/getting-started/_index.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/sources/getting-started/_index.md b/docs/sources/getting-started/_index.md index ebda5f5aeea..6252d05cd40 100644 --- a/docs/sources/getting-started/_index.md +++ b/docs/sources/getting-started/_index.md @@ -6,12 +6,14 @@ labels: products: - enterprise - oss -title: Get started +title: Get started with Grafana Open Source weight: 9 --- -# Get started +# Get started with Grafana Open Source -This section provides guidance on how build your first dashboard after you have installed Grafana. It also provides step-by-step instructions on how to add a Prometheus, InfluxDB, or an MS SQL Server data source. Refer to [Data sources]({{< relref "../datasources" >}}) for a list of all supported data sources. +Grafana helps you collect, correlate, and visualize data with beautiful dashboards — the open source data visualization and monitoring solution that drives informed decisions, enhances system performance, and streamlines troubleshooting. + +This section provides guidance to our open source community about how to build your first dashboard after you have installed Grafana. It also provides step-by-step instructions on how to add a Prometheus, InfluxDB, or an MS SQL Server data source. If you are connecting a different data source, please refer to our complete list of supported [Data sources]({{< relref "../datasources" >}}). If you would like to learn how to get started with Grafana Cloud, our fully managed observability stack, visit the [Grafana Cloud documentation](https://grafana.com/docs/grafana-cloud/quickstart/) for more information. {{< section >}} From 162a422f0a8d9516d0a4db9771b2c470b124b00d Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Tue, 24 Oct 2023 10:19:17 -0400 Subject: [PATCH 028/180] K8s: Playlist apply fix (#76971) --- pkg/services/grafana-apiserver/service.go | 28 +++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/pkg/services/grafana-apiserver/service.go b/pkg/services/grafana-apiserver/service.go index 9a6bdac055b..6f8010e75ac 100644 --- a/pkg/services/grafana-apiserver/service.go +++ b/pkg/services/grafana-apiserver/service.go @@ -10,7 +10,6 @@ import ( "github.com/go-logr/logr" "github.com/grafana/dskit/services" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" @@ -163,7 +162,19 @@ func (s *service) start(ctx context.Context) error { logger.Error(err, "failed to set log level") } - o := options.NewRecommendedOptions("", unstructured.UnstructuredJSONScheme) + // Get the list of groups the server will support + builders := s.builders + + groupVersions := make([]schema.GroupVersion, 0, len(builders)) + // Install schemas + for _, b := range builders { + groupVersions = append(groupVersions, b.GetGroupVersion()) + if err := b.InstallSchema(Scheme); err != nil { + return err + } + } + + o := options.NewRecommendedOptions("/registry/grafana.app", Codecs.LegacyCodec(groupVersions...)) o.SecureServing.BindAddress = s.config.ip o.SecureServing.BindPort = s.config.port o.Authentication.RemoteKubeConfigFileOptional = true @@ -209,6 +220,9 @@ func (s *service) start(ctx context.Context) error { } if o.Etcd != nil { + if err := o.Etcd.Complete(serverConfig.Config.StorageObjectCountTracker, serverConfig.Config.DrainedNotify(), serverConfig.Config.AddPostStartHook); err != nil { + return err + } if err := o.Etcd.ApplyTo(&serverConfig.Config); err != nil { return err } @@ -216,16 +230,6 @@ func (s *service) start(ctx context.Context) error { serverConfig.Authorization.Authorizer = s.authorizer - // Get the list of groups the server will support - builders := s.builders - - // Install schemas - for _, b := range builders { - if err := b.InstallSchema(Scheme); err != nil { - return err - } - } - // Add OpenAPI specs for each group+version defsGetter := getOpenAPIDefinitions(builders) serverConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig( From dfc1875061f03dde4b399d433b7795f117706f26 Mon Sep 17 00:00:00 2001 From: Hugo Kiyodi Oshiro Date: Tue, 24 Oct 2023 16:21:37 +0200 Subject: [PATCH 029/180] Plugins: Add managed instance installation resources (#76767) * Plugins: Add configs to allow managed install * Expose methods to use with cloud plugin installer * Change plugins installer bind to OSS --- conf/defaults.ini | 2 ++ pkg/plugins/manager/fakes/fakes.go | 8 ++++++++ pkg/plugins/manager/installer.go | 4 ++-- pkg/plugins/repo/client.go | 6 +++--- pkg/plugins/repo/ifaces.go | 2 ++ pkg/plugins/repo/service.go | 6 +++--- pkg/server/wireexts_oss.go | 3 +++ pkg/services/pluginsintegration/pluginsintegration.go | 3 --- pkg/setting/setting.go | 1 + pkg/setting/setting_plugins.go | 3 +++ 10 files changed, 27 insertions(+), 11 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index f41d4c95cd2..33ef56bce97 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1505,6 +1505,8 @@ public_key_retrieval_disabled = false public_key_retrieval_on_startup = false # Enter a comma-separated list of plugin identifiers to avoid loading (including core plugins). These plugins will be hidden in the catalog. disable_plugins = +# Auth token for plugin installations and removal in managed instances +install_token = #################################### Grafana Live ########################################## [live] diff --git a/pkg/plugins/manager/fakes/fakes.go b/pkg/plugins/manager/fakes/fakes.go index 7d6d9b5ee64..908a3cfd506 100644 --- a/pkg/plugins/manager/fakes/fakes.go +++ b/pkg/plugins/manager/fakes/fakes.go @@ -204,6 +204,7 @@ type FakePluginRepo struct { GetPluginArchiveFunc func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginArchive, error) GetPluginArchiveByURLFunc func(_ context.Context, archiveURL string, _ repo.CompatOpts) (*repo.PluginArchive, error) GetPluginArchiveInfoFunc func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginArchiveInfo, error) + PluginVersionFunc func(pluginID, version string, compatOpts repo.CompatOpts) (repo.VersionData, error) } // GetPluginArchive fetches the requested plugin archive. @@ -232,6 +233,13 @@ func (r *FakePluginRepo) GetPluginArchiveInfo(ctx context.Context, pluginID, ver return &repo.PluginArchiveInfo{}, nil } +func (r *FakePluginRepo) PluginVersion(pluginID, version string, compatOpts repo.CompatOpts) (repo.VersionData, error) { + if r.PluginVersionFunc != nil { + return r.PluginVersionFunc(pluginID, version, compatOpts) + } + return repo.VersionData{}, nil +} + type FakePluginStorage struct { ExtractFunc func(_ context.Context, pluginID string, dirNameFunc storage.DirNameGeneratorFunc, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) } diff --git a/pkg/plugins/manager/installer.go b/pkg/plugins/manager/installer.go index 8e8f5ce9aad..249b01b8a5b 100644 --- a/pkg/plugins/manager/installer.go +++ b/pkg/plugins/manager/installer.go @@ -45,7 +45,7 @@ func New(pluginRegistry registry.Service, pluginLoader loader.Service, pluginRep } func (m *PluginInstaller) Add(ctx context.Context, pluginID, version string, opts plugins.CompatOpts) error { - compatOpts, err := repoCompatOpts(opts) + compatOpts, err := RepoCompatOpts(opts) if err != nil { return err } @@ -169,7 +169,7 @@ func (m *PluginInstaller) plugin(ctx context.Context, pluginID string) (*plugins return p, true } -func repoCompatOpts(opts plugins.CompatOpts) (repo.CompatOpts, error) { +func RepoCompatOpts(opts plugins.CompatOpts) (repo.CompatOpts, error) { os := opts.OS() arch := opts.Arch() if len(os) == 0 || len(arch) == 0 { diff --git a/pkg/plugins/repo/client.go b/pkg/plugins/repo/client.go index ae366afa2e8..661b752b7f2 100644 --- a/pkg/plugins/repo/client.go +++ b/pkg/plugins/repo/client.go @@ -29,8 +29,8 @@ type Client struct { func NewClient(skipTLSVerify bool, logger log.PrettyLogger) *Client { return &Client{ - httpClient: makeHttpClient(skipTLSVerify, 10*time.Second), - httpClientNoTimeout: makeHttpClient(skipTLSVerify, 0), + httpClient: MakeHttpClient(skipTLSVerify, 10*time.Second), + httpClientNoTimeout: MakeHttpClient(skipTLSVerify, 0), log: logger, } } @@ -234,7 +234,7 @@ func (c *Client) handleResp(res *http.Response, compatOpts CompatOpts) (io.ReadC return res.Body, nil } -func makeHttpClient(skipTLSVerify bool, timeout time.Duration) http.Client { +func MakeHttpClient(skipTLSVerify bool, timeout time.Duration) http.Client { return http.Client{ Timeout: timeout, Transport: &http.Transport{ diff --git a/pkg/plugins/repo/ifaces.go b/pkg/plugins/repo/ifaces.go index 1f63010a285..c68843f7f79 100644 --- a/pkg/plugins/repo/ifaces.go +++ b/pkg/plugins/repo/ifaces.go @@ -14,6 +14,8 @@ type Service interface { GetPluginArchiveByURL(ctx context.Context, archiveURL string, opts CompatOpts) (*PluginArchive, error) // GetPluginArchiveInfo fetches information needed for downloading the requested plugin. GetPluginArchiveInfo(ctx context.Context, pluginID, version string, opts CompatOpts) (*PluginArchiveInfo, error) + // PluginVersion will return plugin version based on the requested information. + PluginVersion(pluginID, version string, compatOpts CompatOpts) (VersionData, error) } type CompatOpts struct { diff --git a/pkg/plugins/repo/service.go b/pkg/plugins/repo/service.go index b6422a1dd8a..0bd1b7a7e6b 100644 --- a/pkg/plugins/repo/service.go +++ b/pkg/plugins/repo/service.go @@ -63,7 +63,7 @@ func (m *Manager) GetPluginArchiveByURL(ctx context.Context, pluginZipURL string // GetPluginArchiveInfo returns the options for downloading the requested plugin (with optional `version`) func (m *Manager) GetPluginArchiveInfo(_ context.Context, pluginID, version string, compatOpts CompatOpts) (*PluginArchiveInfo, error) { - v, err := m.pluginVersion(pluginID, version, compatOpts) + v, err := m.PluginVersion(pluginID, version, compatOpts) if err != nil { return nil, err } @@ -75,8 +75,8 @@ func (m *Manager) GetPluginArchiveInfo(_ context.Context, pluginID, version stri }, nil } -// pluginVersion will return plugin version based on the requested information -func (m *Manager) pluginVersion(pluginID, version string, compatOpts CompatOpts) (VersionData, error) { +// PluginVersion will return plugin version based on the requested information +func (m *Manager) PluginVersion(pluginID, version string, compatOpts CompatOpts) (VersionData, error) { versions, err := m.grafanaCompatiblePluginVersions(pluginID, compatOpts) if err != nil { return VersionData{}, err diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 9460f2d6595..0c4082fa520 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -8,6 +8,7 @@ import ( "github.com/google/wire" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/registry/backgroundsvcs" "github.com/grafana/grafana/pkg/registry/usagestatssvcs" @@ -94,6 +95,8 @@ var wireExtsBasicSet = wire.NewSet( wire.Bind(new(secrets.Migrator), new(*secretsMigrator.SecretsMigrator)), idimpl.ProvideLocalSigner, wire.Bind(new(auth.IDSigner), new(*idimpl.LocalSigner)), + manager.ProvideInstaller, + wire.Bind(new(plugins.Installer), new(*manager.PluginInstaller)), ) var wireExtsSet = wire.NewSet( diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index 8fd1143ad08..3ba43f36c76 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" pCfg "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" - "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/filestore" pluginLoader "github.com/grafana/grafana/pkg/plugins/manager/loader" @@ -90,8 +89,6 @@ var WireSet = wire.NewSet( wire.Bind(new(pluginerrs.SignatureErrorTracker), new(*pluginerrs.SignatureErrorRegistry)), pluginerrs.ProvideStore, wire.Bind(new(plugins.ErrorResolver), new(*pluginerrs.Store)), - manager.ProvideInstaller, - wire.Bind(new(plugins.Installer), new(*manager.PluginInstaller)), registry.ProvideService, wire.Bind(new(registry.Service), new(*registry.InMemory)), repo.ProvideService, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 48e39f2f411..75c72d471b4 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -246,6 +246,7 @@ type Cfg struct { PluginForcePublicKeyDownload bool PluginSkipPublicKeyDownload bool DisablePlugins []string + PluginInstallToken string PluginsCDNURLTemplate string PluginLogBackendRequests bool diff --git a/pkg/setting/setting_plugins.go b/pkg/setting/setting_plugins.go index 29b8e9102b1..de2e047289f 100644 --- a/pkg/setting/setting_plugins.go +++ b/pkg/setting/setting_plugins.go @@ -64,5 +64,8 @@ func (cfg *Cfg) readPluginSettings(iniFile *ini.File) error { cfg.PluginsCDNURLTemplate = strings.TrimRight(pluginsSection.Key("cdn_base_url").MustString(""), "/") cfg.PluginLogBackendRequests = pluginsSection.Key("log_backend_requests").MustBool(false) + // Installation token for managed plugins + cfg.PluginInstallToken = pluginsSection.Key("install_token").MustString("") + return nil } From 0fec32e6c179ab279d9e2e26ac45be4233d6e546 Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 17:34:07 +0300 Subject: [PATCH 030/180] Changelog: Updated changelog for 10.2.0 (#77058) Co-authored-by: grafanabot --- CHANGELOG.md | 321 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1f8c8919ea..e26adac50ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,324 @@ + + +# 10.2.0 (2023-10-24) + +### Features and enhancements + +- **Canvas:** Promote Button to beta. [#76582](https://github.com/grafana/grafana/issues/76582), [@adela-almasan](https://github.com/adela-almasan) +- **BarChart:** Improve data links UX in tooltip. [#76514](https://github.com/grafana/grafana/issues/76514), [@torkelo](https://github.com/torkelo) +- **PluginExtensions:** Make sure to pass default timeZone in context. [#76513](https://github.com/grafana/grafana/issues/76513), [@mckn](https://github.com/mckn) +- **PublicDashboards:** Enable feature by default for GA and remove public preview text. [#76484](https://github.com/grafana/grafana/issues/76484), [@juanicabanas](https://github.com/juanicabanas) +- **Grafana UI:** Add Avatar component. [#76429](https://github.com/grafana/grafana/issues/76429), [@Clarity-89](https://github.com/Clarity-89) +- **Alerting:** Add support for msteams contact point in external Alertmanagers. [#76392](https://github.com/grafana/grafana/issues/76392), [@alexweav](https://github.com/alexweav) +- **Alerting:** Enable Insights landing page. [#76381](https://github.com/grafana/grafana/issues/76381), [@VikaCep](https://github.com/VikaCep) +- **Transformations:** De-emphasize non-applicable transformations. [#76373](https://github.com/grafana/grafana/issues/76373), [@codeincarnate](https://github.com/codeincarnate) +- **Explore:** Use short units in graphs. [#76358](https://github.com/grafana/grafana/issues/76358), [@Elfo404](https://github.com/Elfo404) +- **Auth:** Enable `None` role for 10.2. [#76343](https://github.com/grafana/grafana/issues/76343), [@eleijonmarck](https://github.com/eleijonmarck) +- **Transformations:** Add context to transformation editor. [#76317](https://github.com/grafana/grafana/issues/76317), [@mdvictor](https://github.com/mdvictor) +- **Transformations:** Add support for setting timezone in Format time and Convert field type transformations. [#76316](https://github.com/grafana/grafana/issues/76316), [@codeincarnate](https://github.com/codeincarnate) +- **Playlist:** Add create+update timestamps to the database. [#76295](https://github.com/grafana/grafana/issues/76295), [@ryantxu](https://github.com/ryantxu) +- **Live:** Allow setting the engine password. [#76289](https://github.com/grafana/grafana/issues/76289), [@jcalisto](https://github.com/jcalisto) +- **Auth:** Add support for role mapping and allowed groups in Google OIDC. [#76266](https://github.com/grafana/grafana/issues/76266), [@Jguer](https://github.com/Jguer) +- **Alerting:** Add provenance field to /api/v1/provisioning/alert-rules. [#76252](https://github.com/grafana/grafana/issues/76252), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Plugins:** Add status_source label to plugin request metrics. [#76236](https://github.com/grafana/grafana/issues/76236), [@xnyo](https://github.com/xnyo) +- **PluginExtensions:** Made it possible to control modal size from extension. [#76232](https://github.com/grafana/grafana/issues/76232), [@mckn](https://github.com/mckn) +- **Loki:** Change run query button text based on number of queries. [#76196](https://github.com/grafana/grafana/issues/76196), [@ivanahuckova](https://github.com/ivanahuckova) +- **CloudWatch Logs:** Add pattern command to syntax. [#76152](https://github.com/grafana/grafana/issues/76152), [@iwysiu](https://github.com/iwysiu) +- **Caching:** Add feature toggle for memory efficient cache payload serialization. [#76145](https://github.com/grafana/grafana/issues/76145), [@mmandrus](https://github.com/mmandrus) +- **Flamegraph:** Make color by package the default color mode. [#76137](https://github.com/grafana/grafana/issues/76137), [@aocenas](https://github.com/aocenas) +- **Service Accounts:** Enable adding folder, dashboard and data source permissions to service accounts. [#76133](https://github.com/grafana/grafana/issues/76133), [@Jguer](https://github.com/Jguer) +- **SparklineCell:** Display absolute value. [#76125](https://github.com/grafana/grafana/issues/76125), [@domasx2](https://github.com/domasx2) +- **FeatureToggle:** Add awsDatasourcesNewFormStyling feature toggle. [#76110](https://github.com/grafana/grafana/issues/76110), [@idastambuk](https://github.com/idastambuk) +- **CloudWatch:** Add missing AWS/Transfer metrics. [#76079](https://github.com/grafana/grafana/issues/76079), [@jangaraj](https://github.com/jangaraj) +- **Transformations:** Add variable support to join by field. [#76056](https://github.com/grafana/grafana/issues/76056), [@oscarkilhed](https://github.com/oscarkilhed) +- **Alerting:** Add rules export on a folder level. [#76016](https://github.com/grafana/grafana/issues/76016), [@konrad147](https://github.com/konrad147) +- **PanelConfig:** Add option to calculate min/max per field instead of using the global min/max in the data frame. [#75952](https://github.com/grafana/grafana/issues/75952), [@oscarkilhed](https://github.com/oscarkilhed) +- **Transformations:** Add unary operations to Add field from calculation. [#75946](https://github.com/grafana/grafana/issues/75946), [@mdvictor](https://github.com/mdvictor) +- **Bar Gauge:** Add field name placement option. [#75932](https://github.com/grafana/grafana/issues/75932), [@nmarrs](https://github.com/nmarrs) +- **AzureMonitor:** Azure Monitor Cheat sheet. [#75931](https://github.com/grafana/grafana/issues/75931), [@alyssabull](https://github.com/alyssabull) +- **Chore:** Bump grafana-plugin-sdk-go to v0.179.0. [#75886](https://github.com/grafana/grafana/issues/75886), [@leandro-deveikis](https://github.com/leandro-deveikis) +- **Dashboards:** Add template variables to selectable options. [#75870](https://github.com/grafana/grafana/issues/75870), [@fabrizio-grafana](https://github.com/fabrizio-grafana) +- **Docs:** Update RBAC documentation. [#75869](https://github.com/grafana/grafana/issues/75869), [@mgyongyosi](https://github.com/mgyongyosi) +- **Alerting:** Export of contact points to HCL. [#75849](https://github.com/grafana/grafana/issues/75849), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **BrowseDashboards:** Enable new Browse Dashboards UI by default. [#75822](https://github.com/grafana/grafana/issues/75822), [@joshhunt](https://github.com/joshhunt) +- **Alerting:** Use new endpoints in the Modify Export. [#75796](https://github.com/grafana/grafana/issues/75796), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Transformations:** Rename "Transform" tab to "Transform data". [#75757](https://github.com/grafana/grafana/issues/75757), [@codeincarnate](https://github.com/codeincarnate) +- **Loki:** Support X-ray as internal link in derived fields. [#75756](https://github.com/grafana/grafana/issues/75756), [@harshabaddam](https://github.com/harshabaddam) +- **Table:** Make sparkline cell respect no value option. [#75750](https://github.com/grafana/grafana/issues/75750), [@oscarkilhed](https://github.com/oscarkilhed) +- **Transformations:** Extended support for variables in filter by name. [#75734](https://github.com/grafana/grafana/issues/75734), [@oscarkilhed](https://github.com/oscarkilhed) +- **Tempo:** TraceQL results as a spans list. [#75660](https://github.com/grafana/grafana/issues/75660), [@adrapereira](https://github.com/adrapereira) +- **Transformations:** Add naming mode to partition by value. [#75650](https://github.com/grafana/grafana/issues/75650), [@oscarkilhed](https://github.com/oscarkilhed) +- **Transformations:** Correct description of rename by regex. [#75641](https://github.com/grafana/grafana/issues/75641), [@oscarkilhed](https://github.com/oscarkilhed) +- **Team:** Support `sort` query param for teams search endpoint. [#75622](https://github.com/grafana/grafana/issues/75622), [@gamab](https://github.com/gamab) +- **CloudWatch Logs:** Make monaco query editor general availability. [#75589](https://github.com/grafana/grafana/issues/75589), [@iwysiu](https://github.com/iwysiu) +- **Explore:** Improve timeseries limit disclaimer. [#75587](https://github.com/grafana/grafana/issues/75587), [@Elfo404](https://github.com/Elfo404) +- **Stat:** Disable wide layout. [#75556](https://github.com/grafana/grafana/issues/75556), [@nmarrs](https://github.com/nmarrs) +- **DataSourceAPI:** Add adhoc filters to DataQueryRequest and make it not depend on global templateSrv. [#75552](https://github.com/grafana/grafana/issues/75552), [@torkelo](https://github.com/torkelo) +- **Playlist:** Remove unused/deprecated api and unused wrapper. [#75503](https://github.com/grafana/grafana/issues/75503), [@ryantxu](https://github.com/ryantxu) +- **Explore:** Make Explore Toolbar sticky. [#75500](https://github.com/grafana/grafana/issues/75500), [@harisrozajac](https://github.com/harisrozajac) +- **Elasticsearch:** Added support for calendar_interval in ES date histogram queries. [#75459](https://github.com/grafana/grafana/issues/75459), [@NikolayTsvetkov](https://github.com/NikolayTsvetkov) +- **Alerting:** Manage remote Alertmanager silences. [#75452](https://github.com/grafana/grafana/issues/75452), [@santihernandezc](https://github.com/santihernandezc) +- **TimeSeries:** Implement ad hoc y-zoom via Shift-drag. [#75408](https://github.com/grafana/grafana/issues/75408), [@leeoniya](https://github.com/leeoniya) +- **Cloudwatch:** Add missing AWS regions. [#75392](https://github.com/grafana/grafana/issues/75392), [@SijmenHuizenga](https://github.com/SijmenHuizenga) +- **Transformations:** Add support for dashboard variable in limit, sort by, filter by value, heatmap and histogram. [#75372](https://github.com/grafana/grafana/issues/75372), [@oscarkilhed](https://github.com/oscarkilhed) +- **GrafanaUI:** Smaller padding around Drawer's title, subtitle, and tabs. [#75354](https://github.com/grafana/grafana/issues/75354), [@polibb](https://github.com/polibb) +- **InteractiveTable:** Add controlled sort. [#75289](https://github.com/grafana/grafana/issues/75289), [@Clarity-89](https://github.com/Clarity-89) +- **Feature Toggles API:** Trigger webhook call when updating. [#75254](https://github.com/grafana/grafana/issues/75254), [@jcalisto](https://github.com/jcalisto) +- **Trace View:** Span list visual update. [#75238](https://github.com/grafana/grafana/issues/75238), [@adrapereira](https://github.com/adrapereira) +- **User:** Support `sort` query param for user and org user, search endpoints. [#75229](https://github.com/grafana/grafana/issues/75229), [@gamab](https://github.com/gamab) +- **Admin:** Use backend sort. [#75228](https://github.com/grafana/grafana/issues/75228), [@Clarity-89](https://github.com/Clarity-89) +- **Breadcrumbs:** Enable plugins to override breadcrumbs that are generated by pages defined in plugin.json. [#75218](https://github.com/grafana/grafana/issues/75218), [@torkelo](https://github.com/torkelo) +- **Cloudwatch:** Add Documentation on Temporary Credentials. [#75178](https://github.com/grafana/grafana/issues/75178), [@sarahzinger](https://github.com/sarahzinger) +- **Tracing:** Span filters reset show matches only. [#75150](https://github.com/grafana/grafana/issues/75150), [@joey-grafana](https://github.com/joey-grafana) +- **Toggle:** Enable Recorded Queries Multi support by default. [#75097](https://github.com/grafana/grafana/issues/75097), [@kylebrandt](https://github.com/kylebrandt) +- **GrafanaUI:** Support memoization of useStyles additional arguments. [#75000](https://github.com/grafana/grafana/issues/75000), [@joshhunt](https://github.com/joshhunt) +- **NodeGraph:** Allow to set node radius in dataframe. [#74963](https://github.com/grafana/grafana/issues/74963), [@piggito](https://github.com/piggito) +- **AdhocFilters:** Improve typing and signature of getTagKeys and getTagValues and behaviors. [#74962](https://github.com/grafana/grafana/issues/74962), [@torkelo](https://github.com/torkelo) +- **OpenSearch:** Add timeRange to parameters passed to getTagValues. [#74952](https://github.com/grafana/grafana/issues/74952), [@iwysiu](https://github.com/iwysiu) +- **PublicDashboards:** Refresh ds plugin supported list. [#74947](https://github.com/grafana/grafana/issues/74947), [@juanicabanas](https://github.com/juanicabanas) +- **Chore:** Update metrics for AWS/MediaConnect. [#74946](https://github.com/grafana/grafana/issues/74946), [@Deepali1211](https://github.com/Deepali1211) +- **Tempo:** Added not regex operator. [#74907](https://github.com/grafana/grafana/issues/74907), [@adrapereira](https://github.com/adrapereira) +- **MySQL:** Update configuration page styling. [#74902](https://github.com/grafana/grafana/issues/74902), [@gwdawson](https://github.com/gwdawson) +- **InteractiveTable:** Add horizontal scroll. [#74888](https://github.com/grafana/grafana/issues/74888), [@Clarity-89](https://github.com/Clarity-89) +- **SSE:** Reduce to apply Mode to instant vector (mathexp.Number). [#74859](https://github.com/grafana/grafana/issues/74859), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **CloudWatch:** Correctly add dimension values to labels. [#74847](https://github.com/grafana/grafana/issues/74847), [@iwysiu](https://github.com/iwysiu) +- **Alerting:** Add export drawer when exporting all Grafana managed alerts. [#74846](https://github.com/grafana/grafana/issues/74846), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Feature:** Allow to disable a plugin. [#74840](https://github.com/grafana/grafana/issues/74840), [@andresmgot](https://github.com/andresmgot) +- **Alerting:** Always show expression warnings and errors. [#74839](https://github.com/grafana/grafana/issues/74839), [@gillesdemey](https://github.com/gillesdemey) +- **Tempo:** Added spss config - spans per span set. [#74832](https://github.com/grafana/grafana/issues/74832), [@adrapereira](https://github.com/adrapereira) +- **Admin:** Use InteractiveTable for user and team tables. [#74821](https://github.com/grafana/grafana/issues/74821), [@Clarity-89](https://github.com/Clarity-89) +- **Canvas:** Button API Editor support template variables. [#74779](https://github.com/grafana/grafana/issues/74779), [@adela-almasan](https://github.com/adela-almasan) +- **PublicDashboards:** Title logo and footer redesign. [#74769](https://github.com/grafana/grafana/issues/74769), [@juanicabanas](https://github.com/juanicabanas) +- **Tempo:** Highlight errors in TraceQL query. [#74697](https://github.com/grafana/grafana/issues/74697), [@fabrizio-grafana](https://github.com/fabrizio-grafana) +- **Folders:** Do not allow modifying the folder UID via the API. [#74684](https://github.com/grafana/grafana/issues/74684), [@papagian](https://github.com/papagian) +- **Pyroscope:** Remove support for old pyroscope. [#74683](https://github.com/grafana/grafana/issues/74683), [@aocenas](https://github.com/aocenas) +- **AzureMonitor:** Improve Log Analytics query efficiency. [#74675](https://github.com/grafana/grafana/issues/74675), [@aangelisc](https://github.com/aangelisc) +- **Canvas:** Button API Editor support setting parameters. [#74637](https://github.com/grafana/grafana/issues/74637), [@adela-almasan](https://github.com/adela-almasan) +- **Alerting:** Support for single rule and multi-folder rule export. [#74625](https://github.com/grafana/grafana/issues/74625), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Loki:** Added query editor and builder support for new Logfmt features. [#74619](https://github.com/grafana/grafana/issues/74619), [@matyax](https://github.com/matyax) +- **Alerting:** Add export drawer with yaml and json formats, in policies and contact points view. [#74613](https://github.com/grafana/grafana/issues/74613), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Canvas:** Button API - Add support for GET requests. [#74566](https://github.com/grafana/grafana/issues/74566), [@adela-almasan](https://github.com/adela-almasan) +- **Explore:** Content Outline. [#74536](https://github.com/grafana/grafana/issues/74536), [@harisrozajac](https://github.com/harisrozajac) +- **Alerting:** Add Grafana-managed groups and rules export. [#74522](https://github.com/grafana/grafana/issues/74522), [@konrad147](https://github.com/konrad147) +- **Plugins:** Unset annotation editor variables. [#74519](https://github.com/grafana/grafana/issues/74519), [@oshirohugo](https://github.com/oshirohugo) +- **Internationalization:** Set lang of HTML page to user language preference. [#74513](https://github.com/grafana/grafana/issues/74513), [@ypnos](https://github.com/ypnos) +- **Chore:** Remove unused/deprecated method. [#74485](https://github.com/grafana/grafana/issues/74485), [@ryantxu](https://github.com/ryantxu) +- **Logging:** Add `WithContextualAttributes` to pass log params based on the given context. [#74428](https://github.com/grafana/grafana/issues/74428), [@svennergr](https://github.com/svennergr) +- **CloudWatch:** Add AWS/S3 replication metrics (#74416). [#74418](https://github.com/grafana/grafana/issues/74418), [@jordanefillatre](https://github.com/jordanefillatre) +- **Canvas:** New circle/ellipse element. [#74389](https://github.com/grafana/grafana/issues/74389), [@Develer](https://github.com/Develer) +- **Loki:** Add backend healthcheck. [#74330](https://github.com/grafana/grafana/issues/74330), [@svennergr](https://github.com/svennergr) +- **Transformations:** Show row index as percent in 'Add field from calculation'. [#74322](https://github.com/grafana/grafana/issues/74322), [@mdvictor](https://github.com/mdvictor) +- **Geomap:** Add Symbol Alignment Options. [#74293](https://github.com/grafana/grafana/issues/74293), [@drew08t](https://github.com/drew08t) +- **Dashboard:** Auto-generate panel title and description using AI. [#74284](https://github.com/grafana/grafana/issues/74284), [@nmarrs](https://github.com/nmarrs) +- **Alerting:** Adds additional pagination to several views. [#74268](https://github.com/grafana/grafana/issues/74268), [@gillesdemey](https://github.com/gillesdemey) +- **CloudWatch:** Add additional AWS/Firehose metrics for DynamicPartitioning support. [#74237](https://github.com/grafana/grafana/issues/74237), [@tristanburgess](https://github.com/tristanburgess) +- **Chore:** Replace entity GRN with infra/grn GRN. [#74198](https://github.com/grafana/grafana/issues/74198), [@DanCech](https://github.com/DanCech) +- **Dashboard:** Remove old panel code and leave only new panel design. [#74196](https://github.com/grafana/grafana/issues/74196), [@polibb](https://github.com/polibb) +- **Tempo:** Update default editor to TraceQL tab. [#74153](https://github.com/grafana/grafana/issues/74153), [@joey-grafana](https://github.com/joey-grafana) +- **Plugins:** Move filter back to DataSourceWithBackend. [#74147](https://github.com/grafana/grafana/issues/74147), [@ryantxu](https://github.com/ryantxu) +- **Axis:** Add separate show axis option. [#74117](https://github.com/grafana/grafana/issues/74117), [@Develer](https://github.com/Develer) +- **Alerting:** Do not show grouping when grouplabels are empty in email template. [#74090](https://github.com/grafana/grafana/issues/74090), [@gillesdemey](https://github.com/gillesdemey) +- **Currency:** Add Malaysian Ringgit (RM). [#74073](https://github.com/grafana/grafana/issues/74073), [@skangmy](https://github.com/skangmy) +- **Alerting:** Paginate silences table(s). [#74041](https://github.com/grafana/grafana/issues/74041), [@gillesdemey](https://github.com/gillesdemey) +- **Chore:** Update grafana-plugin-sdk-go version. [#74039](https://github.com/grafana/grafana/issues/74039), [@oshirohugo](https://github.com/oshirohugo) +- **Dashboards:** Add "import dashboard" to empty dashboard landing page. [#74018](https://github.com/grafana/grafana/issues/74018), [@ivanortegaalba](https://github.com/ivanortegaalba) +- **Dashlist:** Use new nested folder picker. [#74011](https://github.com/grafana/grafana/issues/74011), [@joshhunt](https://github.com/joshhunt) +- **Plugins:** Add dependency column in version table. [#73991](https://github.com/grafana/grafana/issues/73991), [@oshirohugo](https://github.com/oshirohugo) +- **Elasticsearch:** Unify default value for geo hash grid precision across the code to 3. [#73922](https://github.com/grafana/grafana/issues/73922), [@ivanahuckova](https://github.com/ivanahuckova) +- **Dashboard:** Store original JSON in DashboardModel. [#73881](https://github.com/grafana/grafana/issues/73881), [@Clarity-89](https://github.com/Clarity-89) +- **Grafana/ui:** Expose trigger method from `useForm` to children. [#73831](https://github.com/grafana/grafana/issues/73831), [@javiruiz01](https://github.com/javiruiz01) +- **RBAC:** Enable permission validation by default. [#73804](https://github.com/grafana/grafana/issues/73804), [@mgyongyosi](https://github.com/mgyongyosi) +- **Alerting:** Update provisioning to validate user-defined UID on create. [#73793](https://github.com/grafana/grafana/issues/73793), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Plugins:** Allow async panel migrations. [#73782](https://github.com/grafana/grafana/issues/73782), [@joshhunt](https://github.com/joshhunt) +- **Correlations:** Allow creating correlations for provisioned data sources. [#73737](https://github.com/grafana/grafana/issues/73737), [@ifrost](https://github.com/ifrost) +- **Alerting:** Add contact point for Grafana OnCall. [#73733](https://github.com/grafana/grafana/issues/73733), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Tempo:** Improve autocompletion and syntax highlighting for TraceQL tab. [#73707](https://github.com/grafana/grafana/issues/73707), [@fabrizio-grafana](https://github.com/fabrizio-grafana) +- **Auth:** Make sure that SAML responses with default namespaces are parsed correctly. [#73701](https://github.com/grafana/grafana/issues/73701), [@IevaVasiljeva](https://github.com/IevaVasiljeva) +- **ArrayVector:** Add vector field value warning. [#73692](https://github.com/grafana/grafana/issues/73692), [@Develer](https://github.com/Develer) +- **Loki:** Implement `keep` and `drop` operations. [#73636](https://github.com/grafana/grafana/issues/73636), [@ivanahuckova](https://github.com/ivanahuckova) +- **Explore Logs:** Update log filtering functions to only have effect in the source query. [#73626](https://github.com/grafana/grafana/issues/73626), [@matyax](https://github.com/matyax) +- **Transforms:** Add 'Format String' Transform. [#73624](https://github.com/grafana/grafana/issues/73624), [@sjd210](https://github.com/sjd210) +- **Explore:** Improve handling time range keyboard shortcuts inside Explore. [#73600](https://github.com/grafana/grafana/issues/73600), [@ifrost](https://github.com/ifrost) +- **MSSQL:** Add support for MI authentication to MSSQL. [#73597](https://github.com/grafana/grafana/issues/73597), [@oscarkilhed](https://github.com/oscarkilhed) +- **Tracing:** Support remote, rate-limited, and probabilistic sampling in tracing.opentelemetry config section. [#73587](https://github.com/grafana/grafana/issues/73587), [@hairyhenderson](https://github.com/hairyhenderson) +- **Cloudwatch:** Upgrade grafana-aws-sdk. [#73580](https://github.com/grafana/grafana/issues/73580), [@sarahzinger](https://github.com/sarahzinger) +- **Pyroscope:** Template variable support. [#73572](https://github.com/grafana/grafana/issues/73572), [@aocenas](https://github.com/aocenas) +- **CloudWatch:** Add missing region Middle East (UAE) me-central-1. [#73560](https://github.com/grafana/grafana/issues/73560), [@gelldur](https://github.com/gelldur) +- **Feat:** Feature toggle admin page frontend write UI and InteractiveTable sorting. [#73533](https://github.com/grafana/grafana/issues/73533), [@IbrahimCSAE](https://github.com/IbrahimCSAE) +- **Cloudwatch:** Add back support for old Log Group picker. [#73524](https://github.com/grafana/grafana/issues/73524), [@sarahzinger](https://github.com/sarahzinger) +- **Google Cloud Monitor:** Prom query editor. [#73503](https://github.com/grafana/grafana/issues/73503), [@bossinc](https://github.com/bossinc) +- **Plugins:** Remove deprecated grafana-toolkit. [#73489](https://github.com/grafana/grafana/issues/73489), [@Ukochka](https://github.com/Ukochka) +- **LibraryPanels:** Add RBAC support. [#73475](https://github.com/grafana/grafana/issues/73475), [@kaydelaney](https://github.com/kaydelaney) +- **Chore:** Remove DashboardPickerByID. [#73466](https://github.com/grafana/grafana/issues/73466), [@Clarity-89](https://github.com/Clarity-89) +- **Elastic:** Add `id` field to Elastic responses to allow permalinking. [#73382](https://github.com/grafana/grafana/issues/73382), [@svennergr](https://github.com/svennergr) +- **Correlations:** Add an editor in Explore. [#73315](https://github.com/grafana/grafana/issues/73315), [@gelicia](https://github.com/gelicia) +- **Tempo:** Replace template variables in TraceQL tab when streaming is enabled. [#73259](https://github.com/grafana/grafana/issues/73259), [@fabrizio-grafana](https://github.com/fabrizio-grafana) +- **CloudWatch Logs:** Wrap sync error from executeGetQueryResults. [#73252](https://github.com/grafana/grafana/issues/73252), [@iwysiu](https://github.com/iwysiu) +- **Elasticsearch:** Enable running of queries trough data source backend. [#73222](https://github.com/grafana/grafana/issues/73222), [@ivanahuckova](https://github.com/ivanahuckova) +- **Tempo:** Metrics summary. [#73201](https://github.com/grafana/grafana/issues/73201), [@joey-grafana](https://github.com/joey-grafana) +- **Alerting:** Export of alert rules in HCL format. [#73166](https://github.com/grafana/grafana/issues/73166), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **SSE:** Localize/Contain Errors within an Expression. [#73163](https://github.com/grafana/grafana/issues/73163), [@kylebrandt](https://github.com/kylebrandt) +- **Dashboards:** PanelChrome - remove untitled placeholder and add border when panel is transparent. [#73150](https://github.com/grafana/grafana/issues/73150), [@axelavargas](https://github.com/axelavargas) +- **CloudWatch:** Add missing AppFlow metrics. [#73149](https://github.com/grafana/grafana/issues/73149), [@ciancullinan](https://github.com/ciancullinan) +- **Flamegraph:** Move to package. [#73113](https://github.com/grafana/grafana/issues/73113), [@aocenas](https://github.com/aocenas) +- **Plugins:** Forward feature toggles to plugins. [#72995](https://github.com/grafana/grafana/issues/72995), [@oshirohugo](https://github.com/oshirohugo) +- **SSE:** Group data source node execution by data source. [#72935](https://github.com/grafana/grafana/issues/72935), [@kylebrandt](https://github.com/kylebrandt) +- **Dashboard:** Support template variables in Search tab for Tempo. [#72867](https://github.com/grafana/grafana/issues/72867), [@fabrizio-grafana](https://github.com/fabrizio-grafana) +- **Cloudwatch:** Upgrade aws-sdk and display external ids for temporary credentials. [#72821](https://github.com/grafana/grafana/issues/72821), [@sarahzinger](https://github.com/sarahzinger) +- **Dashboards:** Add megawatt hour (MWh) unit. [#72779](https://github.com/grafana/grafana/issues/72779), [@zuchka](https://github.com/zuchka) +- **Dashboard:** Add support for Tempo query variables. [#72745](https://github.com/grafana/grafana/issues/72745), [@fabrizio-grafana](https://github.com/fabrizio-grafana) +- **Auth:** Add key_id config param to auth.jwt. [#72711](https://github.com/grafana/grafana/issues/72711), [@mgyongyosi](https://github.com/mgyongyosi) +- **Alerting:** Move legacy alert migration from sqlstore migration to service. [#72702](https://github.com/grafana/grafana/issues/72702), [@JacobsonMT](https://github.com/JacobsonMT) +- **Loki:** Introduce `$__auto` range variable for metric queries. [#72690](https://github.com/grafana/grafana/issues/72690), [@ivanahuckova](https://github.com/ivanahuckova) +- **GLDS:** Move Text component from the `unstable` package to `grafana-ui`. [#72660](https://github.com/grafana/grafana/issues/72660), [@eledobleefe](https://github.com/eledobleefe) +- **Datasource Plugins:** Allow tracking for configuration usage. [#72650](https://github.com/grafana/grafana/issues/72650), [@sarahzinger](https://github.com/sarahzinger) +- **Cloudwatch Logs:** Set Alerting timeout to datasource config's logsTimeout (#72611). [#72611](https://github.com/grafana/grafana/issues/72611), [@idastambuk](https://github.com/idastambuk) +- **Flamegraph:** Add nice empty state for dashboard panel. [#72583](https://github.com/grafana/grafana/issues/72583), [@aocenas](https://github.com/aocenas) +- **Explore:** Unified Node Graph Container. [#72558](https://github.com/grafana/grafana/issues/72558), [@harisrozajac](https://github.com/harisrozajac) +- **Tracing:** Split name column in search results. [#72449](https://github.com/grafana/grafana/issues/72449), [@joey-grafana](https://github.com/joey-grafana) +- **Tracing:** Trace to metrics default range. [#72433](https://github.com/grafana/grafana/issues/72433), [@joey-grafana](https://github.com/joey-grafana) +- **Email:** Light theme email templates. [#72398](https://github.com/grafana/grafana/issues/72398), [@gillesdemey](https://github.com/gillesdemey) +- **Correlations:** Add organization id. [#72258](https://github.com/grafana/grafana/issues/72258), [@ifrost](https://github.com/ifrost) +- **Feat:** Feature toggle admin page frontend interface. [#72164](https://github.com/grafana/grafana/issues/72164), [@IbrahimCSAE](https://github.com/IbrahimCSAE) +- **Alerting:** Show annotations markers in TimeSeries panel when using Loki as …. [#72084](https://github.com/grafana/grafana/issues/72084), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Custom contact point for OnCall in Grafana AM. [#72021](https://github.com/grafana/grafana/issues/72021), [@konrad147](https://github.com/konrad147) +- **Frontend:** Allows PanelChrome to be collapsed. [#71991](https://github.com/grafana/grafana/issues/71991), [@harisrozajac](https://github.com/harisrozajac) +- **Elasticsearch:** Implement modify query using a Lucene parser. [#71954](https://github.com/grafana/grafana/issues/71954), [@matyax](https://github.com/matyax) +- **Table:** Support display of multiple sub tables. [#71953](https://github.com/grafana/grafana/issues/71953), [@joey-grafana](https://github.com/joey-grafana) +- **A11y:** Make Annotations and Template Variables list and edit pages responsive . [#71791](https://github.com/grafana/grafana/issues/71791), [@juanicabanas](https://github.com/juanicabanas) +- **Dashboard:** Select the last used data source by default when adding a panel to a dashboard. [#71777](https://github.com/grafana/grafana/issues/71777), [@axelavargas](https://github.com/axelavargas) +- **Trace to logs:** Add service name and namespace to default tags. [#71776](https://github.com/grafana/grafana/issues/71776), [@connorlindsey](https://github.com/connorlindsey) +- **Alerting:** Add new metrics and tracings to state manager and scheduler. [#71398](https://github.com/grafana/grafana/issues/71398), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Alerting:** Add configuration options to migrate to an external Alertmanager. [#71318](https://github.com/grafana/grafana/issues/71318), [@santihernandezc](https://github.com/santihernandezc) +- **Annotations:** Improve updating annotation tags queries. [#71201](https://github.com/grafana/grafana/issues/71201), [@sakjur](https://github.com/sakjur) +- **SSE:** Support hysteresis threshold expression. [#70998](https://github.com/grafana/grafana/issues/70998), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Dashboards:** Add alert and panel icon for dashboards that use Angular plugins. [#70951](https://github.com/grafana/grafana/issues/70951), [@xnyo](https://github.com/xnyo) +- **Chore:** Update ubuntu image to 22.04. [#70719](https://github.com/grafana/grafana/issues/70719), [@orgads](https://github.com/orgads) +- **Auth:** Add support for OIDC RP-Initiated Logout. [#70357](https://github.com/grafana/grafana/issues/70357), [@venkatbvc](https://github.com/venkatbvc) +- **Dashboard:** Field Config - Add CFP franc currency (XPF). [#70036](https://github.com/grafana/grafana/issues/70036), [@smortex](https://github.com/smortex) +- **Auth:** Check id token expiry date. [#69829](https://github.com/grafana/grafana/issues/69829), [@akselleirv](https://github.com/akselleirv) +- **Alerting:** Update Discord settings to treat 'url' as a secure setting. [#69588](https://github.com/grafana/grafana/issues/69588), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Prometheus:** Add $\_\_rate_interval_ms to go along with $\_\_interval_ms. [#69582](https://github.com/grafana/grafana/issues/69582), [@ywwg](https://github.com/ywwg) +- **Alerting:** Update state manager to change all current states in the case when Error\NoData is executed as Ok\Nomal. [#68142](https://github.com/grafana/grafana/issues/68142), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Tempo:** Integrate context aware autocomplete API. [#67845](https://github.com/grafana/grafana/issues/67845), [@adrapereira](https://github.com/adrapereira) +- **GrafanaUI:** Add aria-label prop to RadioButtonGroup. [#67019](https://github.com/grafana/grafana/issues/67019), [@khushijain21](https://github.com/khushijain21) +- **Search API:** Search by folder UID. [#65040](https://github.com/grafana/grafana/issues/65040), [@joshhunt](https://github.com/joshhunt) +- **Alerting:** Migrate old alerting templates to Go templates. [#62911](https://github.com/grafana/grafana/issues/62911), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **TeamGroupSync:** Delete group sync entries on team delete. (Enterprise) +- **ServiceAccounts:** Add SAs to managed permissions. (Enterprise) +- **PublicDashboards:** Title logo config. (Enterprise) +- **Caching:** Make cache payload serialization more resistant to out-of-memory crashes. (Enterprise) +- **Caching:** Change error logs for cache items not found to debug logs. (Enterprise) +- **Chore:** Add test console.warn catch. (Enterprise) +- **Emails:** Light theme. (Enterprise) +- **Reporting:** Switch to using dashboard UID. (Enterprise) +- **Recorded Queries:** Use new DS picker. (Enterprise) +- **Reporting:** Add ability to retry failed rendering requests (public preview). (Enterprise) + +### Bug fixes + +- **Snapshots:** Fix breakage of some panel types due to missing structureRev. [#76586](https://github.com/grafana/grafana/issues/76586), [@leeoniya](https://github.com/leeoniya) +- **Loki:** Fix Autocomplete in stream selector overwriting existing label names, or inserting autocomplete result within label value. [#76485](https://github.com/grafana/grafana/issues/76485), [@gtk-grafana](https://github.com/gtk-grafana) +- **Alerting:** Prevent cleanup of non-empty folders on migration revert. [#76439](https://github.com/grafana/grafana/issues/76439), [@JacobsonMT](https://github.com/JacobsonMT) +- **Flamegraph:** Fix inefficient regex generating error on some function names. [#76377](https://github.com/grafana/grafana/issues/76377), [@aocenas](https://github.com/aocenas) +- **Authn:** Prevent empty username and email during sync. [#76330](https://github.com/grafana/grafana/issues/76330), [@kalleep](https://github.com/kalleep) +- **RBAC:** Fix plugins pages access-control. [#76321](https://github.com/grafana/grafana/issues/76321), [@gamab](https://github.com/gamab) +- **Tabs:** Fixes focus style. [#76246](https://github.com/grafana/grafana/issues/76246), [@torkelo](https://github.com/torkelo) +- **Rendering:** Fix Windows plugin signature check. [#76123](https://github.com/grafana/grafana/issues/76123), [@AgnesToulet](https://github.com/AgnesToulet) +- **Dashboards:** It always detect changes when saving an existing dashboard . [#76116](https://github.com/grafana/grafana/issues/76116), [@ivanortegaalba](https://github.com/ivanortegaalba) +- **Flamegraph:** Fix theme propagation. [#76064](https://github.com/grafana/grafana/issues/76064), [@aocenas](https://github.com/aocenas) +- **Pyroscope:** Fix backend panic when querying out of bounds. [#76053](https://github.com/grafana/grafana/issues/76053), [@aocenas](https://github.com/aocenas) +- **DataSourcePicker:** Disable autocomplete for the search input . [#75898](https://github.com/grafana/grafana/issues/75898), [@ivanortegaalba](https://github.com/ivanortegaalba) +- **Loki:** Cache extracted labels. [#75842](https://github.com/grafana/grafana/issues/75842), [@gtk-grafana](https://github.com/gtk-grafana) +- **Tempo:** Fix service graph menu item links. [#75748](https://github.com/grafana/grafana/issues/75748), [@adrapereira](https://github.com/adrapereira) +- **Flamegraph:** Fix bug where package colors would be altered after focusin on a node. [#75695](https://github.com/grafana/grafana/issues/75695), [@aocenas](https://github.com/aocenas) +- **Legend:** Fix desc sort so NaNs are not display first. [#75685](https://github.com/grafana/grafana/issues/75685), [@nmarrs](https://github.com/nmarrs) +- **Transformations:** Fix bug with calculate field when using reduce and the all values calculation. [#75684](https://github.com/grafana/grafana/issues/75684), [@oscarkilhed](https://github.com/oscarkilhed) +- **Plugins:** Fix sorting issue with expandable rows. [#75553](https://github.com/grafana/grafana/issues/75553), [@fabrizio-grafana](https://github.com/fabrizio-grafana) +- **Alerting:** Show panels within collapsed rows in dashboard picker. [#75490](https://github.com/grafana/grafana/issues/75490), [@VikaCep](https://github.com/VikaCep) +- **Tempo:** Use timezone of selected range for timestamps. [#75438](https://github.com/grafana/grafana/issues/75438), [@fabrizio-grafana](https://github.com/fabrizio-grafana) +- **Flamegraph:** Fix css issues when embedded outside of Grafana. [#75369](https://github.com/grafana/grafana/issues/75369), [@aocenas](https://github.com/aocenas) +- **Alerting:** Make shareable alert rule link work if rule name contains forward slashes. [#75362](https://github.com/grafana/grafana/issues/75362), [@domasx2](https://github.com/domasx2) +- **SQLStore:** Fix race condition in RecursiveQueriesAreSupported. [#75274](https://github.com/grafana/grafana/issues/75274), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Connections:** Make the "Add new Connection" page work without internet access. [#75272](https://github.com/grafana/grafana/issues/75272), [@leventebalogh](https://github.com/leventebalogh) +- **TimeSeries:** Apply selected line style to custom pathBuilders. [#75261](https://github.com/grafana/grafana/issues/75261), [@leeoniya](https://github.com/leeoniya) +- **Alerting:** Fix non-applicable error checks for cloud and recording rules. [#75233](https://github.com/grafana/grafana/issues/75233), [@gillesdemey](https://github.com/gillesdemey) +- **TabsBar:** Fix height so that it aligns with grid, and alignItems center . [#75230](https://github.com/grafana/grafana/issues/75230), [@torkelo](https://github.com/torkelo) +- **Prometheus:** Fix creation of invalid dataframes with exemplars. [#75187](https://github.com/grafana/grafana/issues/75187), [@kylebrandt](https://github.com/kylebrandt) +- **Loki:** Fix filters not being added with multiple expressions and parsers. [#75152](https://github.com/grafana/grafana/issues/75152), [@svennergr](https://github.com/svennergr) +- **Pyroscope:** Fix error when no profile types are returned. [#75143](https://github.com/grafana/grafana/issues/75143), [@aocenas](https://github.com/aocenas) +- **BarChart:** Axes centered zero, borders, and colors. [#75136](https://github.com/grafana/grafana/issues/75136), [@leeoniya](https://github.com/leeoniya) +- **Plugins:** Refresh plugin info after installation. [#75074](https://github.com/grafana/grafana/issues/75074), [@oshirohugo](https://github.com/oshirohugo) +- **LDAP:** FIX Enable users on successfull login . [#75073](https://github.com/grafana/grafana/issues/75073), [@gamab](https://github.com/gamab) +- **XYChart:** Fix numerous axis options. [#75044](https://github.com/grafana/grafana/issues/75044), [@leeoniya](https://github.com/leeoniya) +- **Trace View:** Remove "deployment.environment" default traces 2 logs tag. [#74986](https://github.com/grafana/grafana/issues/74986), [@domasx2](https://github.com/domasx2) +- **Snapshots:** Use appUrl on snapshot list page. [#74944](https://github.com/grafana/grafana/issues/74944), [@evictorero](https://github.com/evictorero) +- **Canvas:** Fix inconsistent element placement when changing element type. [#74942](https://github.com/grafana/grafana/issues/74942), [@linghaoSu](https://github.com/linghaoSu) +- **Connections:** Display the type of the datasource. [#74808](https://github.com/grafana/grafana/issues/74808), [@leventebalogh](https://github.com/leventebalogh) +- **Alerting:** Indicate panels without identifier. [#74746](https://github.com/grafana/grafana/issues/74746), [@gillesdemey](https://github.com/gillesdemey) +- **Notifications:** Don't show toasts after refreshing. [#74712](https://github.com/grafana/grafana/issues/74712), [@joshhunt](https://github.com/joshhunt) +- **Alerting:** Fix default policy timing summary. [#74549](https://github.com/grafana/grafana/issues/74549), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Handle custom dashboard permissions in migration service. [#74504](https://github.com/grafana/grafana/issues/74504), [@JacobsonMT](https://github.com/JacobsonMT) +- **CloudWatch Logs:** Fix log query display name when used with expressions. [#74497](https://github.com/grafana/grafana/issues/74497), [@iwysiu](https://github.com/iwysiu) +- **Dashboards:** Escape tags. [#74437](https://github.com/grafana/grafana/issues/74437), [@fabrizio-grafana](https://github.com/fabrizio-grafana) +- **Cloudwatch:** Fix Unexpected error. [#74420](https://github.com/grafana/grafana/issues/74420), [@sarahzinger](https://github.com/sarahzinger) +- **Transformations:** Fix group by field transformation field name text-overflow. [#74173](https://github.com/grafana/grafana/issues/74173), [@oscarkilhed](https://github.com/oscarkilhed) +- **LDAP:** Disable removed users on login. [#74016](https://github.com/grafana/grafana/issues/74016), [@gamab](https://github.com/gamab) +- **Time Range:** Using relative time takes timezone into account. [#74013](https://github.com/grafana/grafana/issues/74013), [@ashharrison90](https://github.com/ashharrison90) +- **Loki:** Fix filtering with structured metadata. [#73955](https://github.com/grafana/grafana/issues/73955), [@svennergr](https://github.com/svennergr) +- **Dashboard embed:** Use port instead of callbackUrl. [#73883](https://github.com/grafana/grafana/issues/73883), [@Clarity-89](https://github.com/Clarity-89) +- **Alerting:** Fix data source copy when switching alert rule types. [#73854](https://github.com/grafana/grafana/issues/73854), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Fix delete cloud rule from detail page. [#73850](https://github.com/grafana/grafana/issues/73850), [@gillesdemey](https://github.com/gillesdemey) +- **LDAP:** Fix active sync with large quantities of users. [#73834](https://github.com/grafana/grafana/issues/73834), [@gamab](https://github.com/gamab) +- **PublicDashboards:** Data discrepancy fix. Use real datasource plugin when it is a public dashboard. . [#73708](https://github.com/grafana/grafana/issues/73708), [@juanicabanas](https://github.com/juanicabanas) +- **A11y:** Fix exemplar marker accessibility. [#73493](https://github.com/grafana/grafana/issues/73493), [@Develer](https://github.com/Develer) +- **A11y:** Fix resource picker accessibility. [#73488](https://github.com/grafana/grafana/issues/73488), [@Develer](https://github.com/Develer) +- **A11y:** Fix resource cards accessibility. [#73487](https://github.com/grafana/grafana/issues/73487), [@Develer](https://github.com/Develer) +- **Template Variables:** Fix conversion from non standard data to dataFrame. [#73486](https://github.com/grafana/grafana/issues/73486), [@aocenas](https://github.com/aocenas) +- **A11y:** Fix canvas element accessibility. [#73483](https://github.com/grafana/grafana/issues/73483), [@Develer](https://github.com/Develer) +- **Tempo:** Fix [object Object] shown as an Event message in Trace view. [#73473](https://github.com/grafana/grafana/issues/73473), [@aocenas](https://github.com/aocenas) +- **A11y:** Fix canvas setting button accessibility. [#73413](https://github.com/grafana/grafana/issues/73413), [@Develer](https://github.com/Develer) +- **PublicDashboards:** Query order bug fixed. [#73293](https://github.com/grafana/grafana/issues/73293), [@juanicabanas](https://github.com/juanicabanas) +- **DatePicker:** Fix calendar not showing correct selected range when changing time zones. [#73273](https://github.com/grafana/grafana/issues/73273), [@ashharrison90](https://github.com/ashharrison90) +- **Cloud Monitoring:** Support AliasBy property in MQL mode. [#73116](https://github.com/grafana/grafana/issues/73116), [@alyssabull](https://github.com/alyssabull) +- **Alerting:** Fix cloud rules editing. [#72927](https://github.com/grafana/grafana/issues/72927), [@konrad147](https://github.com/konrad147) +- **Dashboard:** Fixes dashboard setting Links overflow. [#72428](https://github.com/grafana/grafana/issues/72428), [@chauchausoup](https://github.com/chauchausoup) +- **A11y:** Fix toggletip predictable focus for keyboard users. [#72100](https://github.com/grafana/grafana/issues/72100), [@ckbedwell](https://github.com/ckbedwell) +- **Gauge:** Add overflow scrolling support for vertical and horizontal orientations. [#71690](https://github.com/grafana/grafana/issues/71690), [@nmarrs](https://github.com/nmarrs) +- **Export:** Remove DS input when dashboard is imported with a lib panel that already exists. [#69412](https://github.com/grafana/grafana/issues/69412), [@juanicabanas](https://github.com/juanicabanas) +- **Auditing and UsageInsights:** FIX Loki configuration to use proxy env variables. (Enterprise) +- **PDF:** Fix parenthesis in dashboard title. (Enterprise) +- **Reporting:** Handle commas in variables. (Enterprise) +- **Caching:** Fix caching metrics being doubled. (Enterprise) + +### Breaking changes + +The deprecated `/playlists/{uid}/dashboards` API endpoint has been removed. Dashboard information can be retrieved from the `/dashboard/...` APIs. Issue [#75503](https://github.com/grafana/grafana/issues/75503) + +The `PUT /api/folders/:uid` endpoint no more supports modifying the folder's `UID`. Issue [#74684](https://github.com/grafana/grafana/issues/74684) + +This is a breaking change as we're removing support for `Intersection` (although it is replaced with an option that is nearly the same). Issue [#74675](https://github.com/grafana/grafana/issues/74675) + + +Removed all components for the old panel header design. Issue [#74196](https://github.com/grafana/grafana/issues/74196) + +### Deprecations + +Correlations created before 10.1.0 do not have an organization id assigned and are treated as global. In some rare cases, it may lead to confusing behavior described in #72259. Organization id is now added when a correlation is created. Any existing correlations without organization id will be kept intact and work as before for backward compatibility during the deprecation period that is set to 6 months after handling organization id is released. After that time, correlations without org_id (or org_id = 0 in the database) will stop showing up in Grafana. + +To migrate existing correlations to handle organization id correctly: + +- re-provision any correlations that were created as part of provisioning +- re-create any correlations created with Admin/Correlations page Issue [#72258](https://github.com/grafana/grafana/issues/72258) + +Starting with 10.2, `parentRowIndex` is deprecated. It will be removed in a future release. From 10.2, sub-tables are supported by adding `FieldType.nestedFrames` to the field that contains the nested data in your dataframe. Issue [#71953](https://github.com/grafana/grafana/issues/71953) + +### Plugin development fixes & changes + +- **Toggletip:** Add support to programmatically close it. [#75846](https://github.com/grafana/grafana/issues/75846), [@adela-almasan](https://github.com/adela-almasan) +- **Drawer:** Make content scroll by default. [#75287](https://github.com/grafana/grafana/issues/75287), [@ashharrison90](https://github.com/ashharrison90) + + # 10.1.5 (2023-10-11) From ed3a34afdc15770e84f149f452ed57abefb32708 Mon Sep 17 00:00:00 2001 From: Horst Gutmann Date: Tue, 24 Oct 2023 16:47:15 +0200 Subject: [PATCH 031/180] chore: Bump version in latest.json (#77061) --- latest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/latest.json b/latest.json index 2e265443c59..def63753f63 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "10.1.4", - "testing": "10.1.4" + "stable": "10.2.0", + "testing": "10.2.0" } From f7445777c7c1d5003411db3260d36ec1609b2850 Mon Sep 17 00:00:00 2001 From: Horst Gutmann Date: Tue, 24 Oct 2023 17:14:08 +0200 Subject: [PATCH 032/180] CI: Temporarily disable arm build-tooling (#77064) --- pkg/build/config/variant.go | 7 +++-- pkg/build/config/versions.go | 17 +++++----- pkg/build/docker/init.go | 2 +- pkg/build/packaging/artifacts.go | 53 ++++++++++++++++---------------- 4 files changed, 42 insertions(+), 37 deletions(-) diff --git a/pkg/build/config/variant.go b/pkg/build/config/variant.go index 4fde6a89a74..6b476ed95e6 100644 --- a/pkg/build/config/variant.go +++ b/pkg/build/config/variant.go @@ -16,9 +16,10 @@ const ( ) var AllVariants = []Variant{ - VariantArmV6, - VariantArmV7, - VariantArmV7Musl, + // https://github.com/golang/go/issues/58425 disabling arm builds until go issue is resolved + // VariantArmV6, + // VariantArmV7, + // VariantArmV7Musl, VariantArm64, VariantArm64Musl, VariantDarwinAmd64, diff --git a/pkg/build/config/versions.go b/pkg/build/config/versions.go index 8decc72593c..ec28fe51b35 100644 --- a/pkg/build/config/versions.go +++ b/pkg/build/config/versions.go @@ -101,9 +101,10 @@ var Versions = VersionMap{ }, ReleaseBranchMode: { Variants: []Variant{ - VariantArmV6, - VariantArmV7, - VariantArmV7Musl, + // https://github.com/golang/go/issues/58425 disabling arm builds until go issue is resolved + // VariantArmV6, + // VariantArmV7, + // VariantArmV7Musl, VariantArm64, VariantArm64Musl, VariantDarwinAmd64, @@ -136,9 +137,10 @@ var Versions = VersionMap{ }, TagMode: { Variants: []Variant{ - VariantArmV6, - VariantArmV7, - VariantArmV7Musl, + // https://github.com/golang/go/issues/58425 disabling arm builds until go issue is resolved + // VariantArmV6, + // VariantArmV7, + // VariantArmV7Musl, VariantArm64, VariantArm64Musl, VariantDarwinAmd64, @@ -155,7 +157,8 @@ var Versions = VersionMap{ Architectures: []Architecture{ ArchAMD64, ArchARM64, - ArchARMv7, + // https://github.com/golang/go/issues/58425 disabling arm builds until go issue is resolved + // ArchARMv7, }, Distribution: []Distribution{ Alpine, diff --git a/pkg/build/docker/init.go b/pkg/build/docker/init.go index c7262613b2b..94fe236aa52 100644 --- a/pkg/build/docker/init.go +++ b/pkg/build/docker/init.go @@ -8,7 +8,7 @@ import ( ) // AllArchs is a list of all supported Docker image architectures. -var AllArchs = []string{"amd64", "armv7", "arm64"} +var AllArchs = []string{"amd64", "arm64"} // emulatorImage is the docker image used as the cross-platform emulator var emulatorImage = "tonistiigi/binfmt:qemu-v7.0.0" diff --git a/pkg/build/packaging/artifacts.go b/pkg/build/packaging/artifacts.go index bdce67170e1..36c1e3258e5 100644 --- a/pkg/build/packaging/artifacts.go +++ b/pkg/build/packaging/artifacts.go @@ -81,32 +81,33 @@ var ArtifactConfigs = []buildArtifact{ Arch: "arm64", urlPostfix: ".linux-arm64.tar.gz", }, - { - Os: debOS, - Arch: "armv7", - urlPostfix: "_armhf.deb", - }, - { - Os: debOS, - Arch: "armv6", - packagePostfix: "-rpi", - urlPostfix: "_armhf.deb", - }, - { - Os: rhelOS, - Arch: "armv7", - urlPostfix: ".armhfp.rpm", - }, - { - Os: "linux", - Arch: "armv6", - urlPostfix: ".linux-armv6.tar.gz", - }, - { - Os: "linux", - Arch: "armv7", - urlPostfix: ".linux-armv7.tar.gz", - }, + // https://github.com/golang/go/issues/58425 disabling arm builds until go issue is resolved + // { + // Os: debOS, + // Arch: "armv7", + // urlPostfix: "_armhf.deb", + // }, + // { + // Os: debOS, + // Arch: "armv6", + // packagePostfix: "-rpi", + // urlPostfix: "_armhf.deb", + // }, + // { + // Os: rhelOS, + // Arch: "armv7", + // urlPostfix: ".armhfp.rpm", + // }, + // { + // Os: "linux", + // Arch: "armv6", + // urlPostfix: ".linux-armv6.tar.gz", + // }, + // { + // Os: "linux", + // Arch: "armv7", + // urlPostfix: ".linux-armv7.tar.gz", + // }, { Os: "darwin", Arch: "amd64", From 97be80d1f084ca62fa4a124862748512a0e8aea6 Mon Sep 17 00:00:00 2001 From: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com> Date: Tue, 24 Oct 2023 08:15:31 -0700 Subject: [PATCH 033/180] Geomap: Fix route layer crosshair share (#76702) * Geomap: Fix route layer crosshair share * Update crosshair styling * Consolidate horizontal and vertical lines * Consolidate clear out * Create const for crosshair color --------- Co-authored-by: nmarrs --- .betterer.results | 3 + .../panel/geomap/layers/data/routeLayer.tsx | 60 ++++++++++++++++--- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/.betterer.results b/.betterer.results index e3f389eba41..d2db76d9161 100644 --- a/.betterer.results +++ b/.betterer.results @@ -7207,6 +7207,9 @@ exports[`better eslint`] = { "public/app/plugins/panel/geomap/layers/data/geojsonDynamic.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "public/app/plugins/panel/geomap/layers/data/routeLayer.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/plugins/panel/geomap/layers/registry.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] diff --git a/public/app/plugins/panel/geomap/layers/data/routeLayer.tsx b/public/app/plugins/panel/geomap/layers/data/routeLayer.tsx index e52dd28a272..286c07c4fbe 100644 --- a/public/app/plugins/panel/geomap/layers/data/routeLayer.tsx +++ b/public/app/plugins/panel/geomap/layers/data/routeLayer.tsx @@ -19,7 +19,7 @@ import { DataHoverEvent, DataHoverClearEvent, DataFrame, - TIME_SERIES_TIME_FIELD_NAME, + FieldType, } from '@grafana/data'; import { alpha } from '@grafana/data/src/themes/colorManipulator'; import { MapLayerOptions, FrameGeometrySourceMode } from '@grafana/schema'; @@ -60,6 +60,15 @@ export const defaultRouteConfig: MapLayerOptions = { tooltip: false, }; +enum mapIndex { + x1 = 0, + y1 = 1, + x2 = 2, + y2 = 3, +} + +const crosshairColor = '#607D8B'; + /** * Map layer configuration for circle overlay */ @@ -190,15 +199,26 @@ export const routeLayer: MapLayerRegistryItem = { // Crosshair layer const crosshairFeature = new Feature({}); - const crosshairRadius = (style.base.lineWidth || 6) + 2; + const hLineFeature = new Feature({}); + const vLineFeature = new Feature({}); + const lineFeatures = [hLineFeature, vLineFeature]; + const crosshairRadius = (style.base.lineWidth || 6) + 3; const crosshairStyle = new Style({ image: new Circle({ radius: crosshairRadius, stroke: new Stroke({ - color: alpha(style.base.color, 0.4), - width: crosshairRadius + 2, + color: alpha(crosshairColor, 1), + width: 1, }), - fill: new Fill({ color: style.base.color }), + fill: new Fill({ color: alpha(crosshairColor, 0.4) }), + }), + }); + const lineStyle = new Style({ + stroke: new Stroke({ + color: crosshairColor, + width: 1, + lineDash: [3, 3], + lineCap: 'square', }), }); @@ -209,8 +229,15 @@ export const routeLayer: MapLayerRegistryItem = { style: crosshairStyle, }); + const linesLayer = new VectorLayer({ + source: new VectorSource({ + features: lineFeatures, + }), + style: lineStyle, + }); + const layer = new LayerGroup({ - layers: [vectorLayer, crosshairLayer], + layers: [vectorLayer, crosshairLayer, linesLayer], }); // Crosshair sharing subscriptions @@ -222,19 +249,35 @@ export const routeLayer: MapLayerRegistryItem = { .pipe(throttleTime(8)) .subscribe({ next: (event) => { + const mapExtents = map.getView().calculateExtent(map.getSize()); const feature = source.getFeatures()[0]; const frame: DataFrame = feature?.get('frame'); const time: number = event.payload?.point?.time; if (frame && time) { - const timeField = frame.fields.find((f) => f.name === TIME_SERIES_TIME_FIELD_NAME); + const timeField = frame.fields.find((f) => f.type === FieldType.time); if (timeField) { const timestamps: number[] = timeField.values; const pointIdx = findNearestTimeIndex(timestamps, time); if (pointIdx !== null) { const out = getGeometryField(frame, location); if (out.field) { - crosshairFeature.setGeometry(out.field.values[pointIdx]); + const crosshairPoint: Point = out.field.values[pointIdx] as Point; + const crosshairPointCoords = crosshairPoint.getCoordinates(); + crosshairFeature.setGeometry(crosshairPoint); crosshairFeature.setStyle(crosshairStyle); + hLineFeature.setGeometry( + new LineString([ + [mapExtents[mapIndex.x1], crosshairPointCoords[mapIndex.y1]], + [mapExtents[mapIndex.x2], crosshairPointCoords[mapIndex.y1]], + ]) + ); + vLineFeature.setGeometry( + new LineString([ + [crosshairPointCoords[mapIndex.x1], mapExtents[mapIndex.y1]], + [crosshairPointCoords[mapIndex.x1], mapExtents[mapIndex.y2]], + ]) + ); + lineFeatures.forEach((feature) => feature.setStyle(lineStyle)); } } } @@ -246,6 +289,7 @@ export const routeLayer: MapLayerRegistryItem = { subscriptions.add( eventBus.subscribe(DataHoverClearEvent, (event) => { crosshairFeature.setStyle(new Style({})); + lineFeatures.forEach((feature) => feature.setStyle(new Style({}))); }) ); From 38996202343abc918d129bdf2d17c6def6997492 Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Tue, 24 Oct 2023 17:46:36 +0200 Subject: [PATCH 034/180] Tempo: Add new structural operators (#77056) --- package.json | 2 +- .../tempo/traceql/TraceQLEditor.test.tsx | 4 ++++ .../datasource/tempo/traceql/autocomplete.ts | 14 ++++++++++++++ yarn.lock | 10 +++++----- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 776f96d4b78..6bba8df20d2 100644 --- a/package.json +++ b/package.json @@ -254,7 +254,7 @@ "@grafana/flamegraph": "workspace:*", "@grafana/google-sdk": "0.1.1", "@grafana/lezer-logql": "0.2.1", - "@grafana/lezer-traceql": "0.0.7", + "@grafana/lezer-traceql": "0.0.8", "@grafana/monaco-logql": "^0.0.7", "@grafana/runtime": "workspace:*", "@grafana/scenes": "^1.18.0", diff --git a/public/app/plugins/datasource/tempo/traceql/TraceQLEditor.test.tsx b/public/app/plugins/datasource/tempo/traceql/TraceQLEditor.test.tsx index 79c1e3a5de0..40091386c4a 100644 --- a/public/app/plugins/datasource/tempo/traceql/TraceQLEditor.test.tsx +++ b/public/app/plugins/datasource/tempo/traceql/TraceQLEditor.test.tsx @@ -72,6 +72,10 @@ describe('Check for syntax errors in query', () => { ['{span.status = $code}'], ['{span.${attribute} = "GET"}'], ['{span.${attribute:format} = ${value:format} }'], + ['{true} >> {true}'], + ['{true} << {true}'], + ['{true} !>> {true}'], + ['{true} !<< {true}'], ])('valid query - %s', (query: string) => { expect(getErrorNodes(query)).toStrictEqual([]); }); diff --git a/public/app/plugins/datasource/tempo/traceql/autocomplete.ts b/public/app/plugins/datasource/tempo/traceql/autocomplete.ts index 2397c0ff63e..9d98bab5037 100644 --- a/public/app/plugins/datasource/tempo/traceql/autocomplete.ts +++ b/public/app/plugins/datasource/tempo/traceql/autocomplete.ts @@ -137,6 +137,20 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP documentation: 'Child operator. Looks for spans matching {condB} that are direct child spans of a parent matching {condA}', }, + { + label: '<<', + insertText: '<<', + detail: 'Ancestor', + documentation: + 'Ancestor operator. Looks for spans matching {condB} that are ancestors of a span matching {condA}', + }, + { + label: '<', + insertText: '<', + detail: 'Parent', + documentation: + 'Parent operator. Looks for spans matching {condB} that are direct parent spans of a child matching {condA}', + }, { label: '~', insertText: '~', diff --git a/yarn.lock b/yarn.lock index 4a01ddcbb86..f5b24299c09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3226,12 +3226,12 @@ __metadata: languageName: node linkType: hard -"@grafana/lezer-traceql@npm:0.0.7": - version: 0.0.7 - resolution: "@grafana/lezer-traceql@npm:0.0.7" +"@grafana/lezer-traceql@npm:0.0.8": + version: 0.0.8 + resolution: "@grafana/lezer-traceql@npm:0.0.8" peerDependencies: "@lezer/lr": ^1.3.0 - checksum: 920f8116d61907e12ca1dd51949242abf435e675105699c7b2fcd8c024999ed0ddfd3500b1802111f3ed9ad37e16f40826790658bdf94224cd5b615e20360ab9 + checksum: cd257010689de4c3177fd3e3bb74460d0d8669b9c25530c4ad1efa3cb2793a73c99f34a38058463b93790d3b80bf585992165801d6fd4b617823f1570c8dffad languageName: node linkType: hard @@ -17799,7 +17799,7 @@ __metadata: "@grafana/flamegraph": "workspace:*" "@grafana/google-sdk": 0.1.1 "@grafana/lezer-logql": 0.2.1 - "@grafana/lezer-traceql": 0.0.7 + "@grafana/lezer-traceql": 0.0.8 "@grafana/monaco-logql": ^0.0.7 "@grafana/runtime": "workspace:*" "@grafana/scenes": ^1.18.0 From 2c2d8bcc74d07963b76fb6c898049438f5055310 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Tue, 24 Oct 2023 16:53:26 +0100 Subject: [PATCH 035/180] Alerting: Feedback on docs (#77068) --- .../alerting/fundamentals/high-availability/_index.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/sources/alerting/fundamentals/high-availability/_index.md b/docs/sources/alerting/fundamentals/high-availability/_index.md index 93f2356c3d4..126842bf186 100644 --- a/docs/sources/alerting/fundamentals/high-availability/_index.md +++ b/docs/sources/alerting/fundamentals/high-availability/_index.md @@ -25,13 +25,11 @@ Grafana Alerting uses the Prometheus model of separating the evaluation of alert {{< figure src="/static/img/docs/alerting/unified/high-availability-ua.png" class="docs-image--no-shadow" max-width= "750px" caption="High availability" >}} -When running multiple instances of Grafana, the operational mode of the alert generator does not change. This means that all alert rules are evaluated on all instances of Grafana. You can think of the evaluation of alert rules as being duplicated. However, this is how Grafana Alerting makes sure that as long as at least one Grafana instance is working, alert rules will still be evaluated and notifications for alerts will still be sent. You will see this duplication in state history, and is a good way to tell if you are using high availability. +When running multiple instances of Grafana, all alert rules are evaluated on all instances. You can think of the evaluation of alert rules as being duplicated. This is how Grafana Alerting makes sure that as long as at least one Grafana instance is working, alert rules will still be evaluated and notifications for alerts will still be sent. You will see this duplication in state history, and is a good way to tell if you are using high availability. -While the alert generator evaluates all alert rules on all instances, Grafana makes a best-effort attempt to avoid sending duplicate notifications. Alertmanager chooses availability over consistency which means that in certain situations notifications can be duplicated or appear out-of-order. Alertmanager takes the opinion that duplicate or out-of-order notifications are better than no notifications, and so it uses a gossip protocol to share information about notifications between Grafana instances instead of a more-consistent but less-available protocol such as two-phase commit, or distributed-consensus protocols such as Raft or Paxos. +While the alert generator evaluates all alert rules on all instances, the alert receiver makes a best-effort attempt to avoid sending duplicate notifications. Alertmanager chooses availability over consistency, which may result in occasional duplicated or out-of-order notifications. It takes the opinion that duplicate or out-of-order notifications are better than no notifications. -The Alertmanager also gossips silences, which means a silence created on one Grafana instance is replicated to all other Grafana instances. - -Both notifications and silences are persisted to the database periodically, and during graceful shut down. +The Alertmanager uses a gossip protocol to share information about notifications between Grafana instances. It also gossips silences, which means a silence created on one Grafana instance is replicated to all other Grafana instances. Both notifications and silences are persisted to the database periodically, and during graceful shut down. It is important to make sure that gossiping is configured and tested. You can find the documentation on how to do that [here][configure-high-availability]. From ec84caf389415ebf8b7a5ce61a002f1302f84fc8 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Tue, 24 Oct 2023 15:56:00 +0000 Subject: [PATCH 036/180] Navigation: Basic e2e tests for docked mega menu (#77000) * Navigation: Basic e2e tests for docked mega menu * give mega menu an e2e selector * make existing tests narrower so menu doesn't default to docked. add test for auto docking at large viewport * use new selector in nondocked menu --- e2e/various-suite/navigation.spec.ts | 47 +++++++++++++++++++ .../src/selectors/components.ts | 1 + .../DockedMegaMenu/MegaMenu.test.tsx | 3 +- .../AppChrome/DockedMegaMenu/MegaMenu.tsx | 3 +- .../AppChrome/MegaMenu/MegaMenu.test.tsx | 3 +- .../AppChrome/MegaMenu/NavBarMenu.tsx | 9 +++- 6 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 e2e/various-suite/navigation.spec.ts diff --git a/e2e/various-suite/navigation.spec.ts b/e2e/various-suite/navigation.spec.ts new file mode 100644 index 00000000000..35ee5ee90f4 --- /dev/null +++ b/e2e/various-suite/navigation.spec.ts @@ -0,0 +1,47 @@ +import { e2e } from '../utils'; +import { fromBaseUrl } from '../utils/support/url'; + +describe('Docked Navigation', () => { + beforeEach(() => { + cy.viewport(1280, 800); + e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); + + cy.visit(fromBaseUrl('/'), { + onBeforeLoad(window) { + window.localStorage.setItem('grafana.featureToggles', 'dockedMegaMenu=1'); + }, + }); + }); + + it('should remain docked when reloading the page', () => { + // Expand, then dock the mega menu + cy.get('[aria-label="Toggle menu"]').click(); + cy.get('[aria-label="Dock menu"]').click(); + + e2e.components.NavMenu.Menu().should('be.visible'); + + cy.reload(); + e2e.components.NavMenu.Menu().should('be.visible'); + }); + + it('should remain docked when navigating to another page', () => { + // Expand, then dock the mega menu + cy.get('[aria-label="Toggle menu"]').click(); + cy.get('[aria-label="Dock menu"]').click(); + + cy.contains('a', 'Administration').click(); + e2e.components.NavMenu.Menu().should('be.visible'); + + cy.contains('a', 'Users').click(); + e2e.components.NavMenu.Menu().should('be.visible'); + }); + + it('should become docked at larger viewport sizes', () => { + e2e.components.NavMenu.Menu().should('not.exist'); + + cy.viewport(1920, 1080); + cy.reload(); + + e2e.components.NavMenu.Menu().should('be.visible'); + }); +}); diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index e658a6134fc..c023ab6cf3f 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -264,6 +264,7 @@ export const Components = { }, }, NavMenu: { + Menu: 'data-testid navigation mega-menu', item: 'data-testid Nav menu item', }, NavToolbar: { diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.test.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.test.tsx index a914fda4cac..5b603aeb12d 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.test.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.test.tsx @@ -5,6 +5,7 @@ import { Router } from 'react-router-dom'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; import { NavModelItem } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { locationService } from '@grafana/runtime'; import { TestProvider } from '../../../../../test/helpers/TestProvider'; @@ -53,7 +54,7 @@ describe('MegaMenu', () => { it('should render component', async () => { setup(); - expect(await screen.findByTestId('navbarmenu')).toBeInTheDocument(); + expect(await screen.findByTestId(selectors.components.NavMenu.Menu)).toBeInTheDocument(); expect(await screen.findByRole('link', { name: 'Section name' })).toBeInTheDocument(); }); diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx index 622426f3485..ca3a90e364e 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx @@ -4,6 +4,7 @@ import React, { forwardRef } from 'react'; import { useLocation } from 'react-router-dom'; import { GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { CustomScrollbar, Icon, IconButton, useStyles2 } from '@grafana/ui'; import { Flex } from '@grafana/ui/src/unstable'; import { useGrafana } from 'app/core/context/GrafanaContext'; @@ -39,7 +40,7 @@ export const MegaMenu = React.memo( }; return ( -
+
{ it('should render component', async () => { setup(); - expect(await screen.findByTestId('navbarmenu')).toBeInTheDocument(); + expect(await screen.findByTestId(selectors.components.NavMenu.Menu)).toBeInTheDocument(); expect(await screen.findByRole('link', { name: 'Section name' })).toBeInTheDocument(); }); diff --git a/public/app/core/components/AppChrome/MegaMenu/NavBarMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/NavBarMenu.tsx index 18e6411a900..6cdb9d308b3 100644 --- a/public/app/core/components/AppChrome/MegaMenu/NavBarMenu.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/NavBarMenu.tsx @@ -6,6 +6,7 @@ import React, { useEffect, useRef, useState } from 'react'; import CSSTransition from 'react-transition-group/CSSTransition'; import { GrafanaTheme2, NavModelItem } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { CustomScrollbar, Icon, IconButton, useTheme2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; @@ -62,7 +63,13 @@ export function NavBarMenu({ activeItem, navItems, searchBarHidden, onClose }: P onExited={onClose} > -
+
Date: Tue, 24 Oct 2023 10:30:10 -0600 Subject: [PATCH 037/180] Transformations: Separate useful doc content for UI use (#75781) * baldm0mma/doc_builder/ add to gitignore * baldm0mma/doc_builder/ add makefile commands * baldm0mma/doc_builder/ add content * baldm0mma/doc_builder/ format content * baldm0mma/doc_builder/ update makefile * baldm0mma/docs_builder/ update content * baldm0mma/doc_builder/ add back content * baldm0mma/doc_builder/ run builder * baldm0mma/doc_builder/ update naming * baldm0mma/doc_builder/ remove unused note * baldm0mma/doc_builder/ update template * baldm0mma/doc_builder/ add new line in makefile * baldm0mma/doc_builder/ rem new line * baldm0mma/doc_builder/ add final line * Much of this full PR was Co-Authored-By: Jack Baldry * baldm0mma/doc_builder/ update readme * baldm0mma/doc_builder/ update guidlines * Update scripts/docs/generate-transformations.ts Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update scripts/docs/generate-transformations.ts Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update scripts/docs/generate-transformations.ts Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update scripts/docs/generate-transformations.ts Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update scripts/docs/generate-transformations.ts Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/README.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/README.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update scripts/docs/generate-transformations.ts Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * baldm0mma/doc_builder/ update concat * baldmomma/doc_builder/ finish concat table * baldm0mma/docs_builder/ update with suggestions * baldm0mma/doc_builder/ update content to use getHelperDocs * baldm0mma/doc_builder/ update calculateField * baldm0mma/doc_builder/ update to byRefId * baldm0mma/doc_builder/ update filterByValue * baldm0mma/doc_builder/ update filterFieldsByName * baldm0mma/doc_builder/ update formatTime * baldm0mma/doc_builder/ update groupBy * baldm0mma/doc_builder/ update groupingToMatrix * baldm0mma/doc_update/ update heatmap * baldm0mma/doc_builder/ update histogram * baldm0mma/doc_builder/ update joinByField * baldm0mma/doc_builder/ update joinByLabels * baldm0mma/doc_builder/ update labelsToFields * baldm0mma/doc_builder/ update limit * baldm0mma/doc_builder/ update merge * baldm0mma/doc_builder/ update organize * baldm0mma/doc_builder/ update partitionByValues * baldm0mma/doc_builder/ update prepareTimeSeries * baldm0mma/doc_builder/ update reduce * baldm0mma/doc_builder/ update renameByRegex * baldm0mma/doc_builder/ update rowsToFields * baldm0mma/doc_builder/ update seriesToRows * baldm0mma/doc_builder/ update sortBy * baldm0mma/doc_builder/ update spatial * baldm0mma/doc_builder/ update timeSeriesTable * baldm0mma/doc_builder/ rerender all * baldm0mma/doc_builder/ update calculateField * baldm0mma/doc_builder/ gitignore conflicts * baldm0mma/doc_builder/ add formatString * baldm0mma/doc_builder/ update vars * baldm0mma/doc_builder/ update naming * baldm0mma/doc_builder/ rerender markdown * Update public/app/features/transformers/docs/content.ts Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * baldm0mma/doc_builder/ update content * baldm0mma/doc_builder/ add to codeownders * baldm0mma/doc_builder/ correct spelling * baldm0mma/doc_builder/ update comment --------- Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> --- .github/CODEOWNERS | 1 + .gitignore | 6 +- docs/Makefile | 8 + docs/README.md | 12 +- .../transform-data/index.md | 474 ++++--- .../app/features/transformers/docs/content.ts | 1224 +++++++++++++++++ scripts/docs/generate-transformations.ts | 177 +++ 7 files changed, 1702 insertions(+), 200 deletions(-) create mode 100644 public/app/features/transformers/docs/content.ts create mode 100644 scripts/docs/generate-transformations.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e78609faf98..c34346eca8f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -513,6 +513,7 @@ cypress.config.js @grafana/grafana-frontend-platform /scripts/verify-repo-update/ @grafana/grafana-delivery scripts/generate-icon-bundle.js @grafana/plugins-platform-frontend @grafana/grafana-frontend-platform +/scripts/docs/generate-transformations.ts @grafana/grafana-bi-squad /scripts/webpack/ @grafana/frontend-ops /scripts/generate-a11y-report.sh @grafana/grafana-frontend-platform .pa11yci.conf.js @grafana/grafana-frontend-platform diff --git a/.gitignore b/.gitignore index b8b8a3ffed0..431a6300f76 100644 --- a/.gitignore +++ b/.gitignore @@ -202,4 +202,8 @@ deployment_tools_config.json .pr-body.txt # Core plugin builds -public/app/plugins/**/dist/ \ No newline at end of file +public/app/plugins/**/dist/ + +# Ignore transpiled JavaScript resulting from the generate-transformations.ts script. +/public/app/features/transformers/docs/*.js +/scripts/docs/generate-transformations.js diff --git a/docs/Makefile b/docs/Makefile index 2eaab546cf3..1a43bfc3f62 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -6,3 +6,11 @@ MAKEFLAGS += --warn-undefined-variables MAKEFLAGS += --no-builtin-rule include docs.mk + +.PHONY: sources/panels-visualizations/query-transform-data/transform-data/index.md +sources/panels-visualizations/query-transform-data/transform-data/index.md: ## Generate the Transform Data page source. +sources/panels-visualizations/query-transform-data/transform-data/index.md: + cd $(CURDIR)/.. + npx tsc ../scripts/docs/generate-transformations.ts + node ../scripts/docs/generate-transformations.js > $(CURDIR)/$@ + npx prettier -w $(CURDIR)/$@ diff --git a/docs/README.md b/docs/README.md index 006bb41449b..e53dd2460ee 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,7 +23,17 @@ If you have the grafana/website repo checked out in the same directory as the gr ## Content guidelines -Edit content in the `sources` directory. +Generally, one can edit content in the `sources` directory. + +NOTE: the following paths are built instead from a typescript file and are auto-generated. Please do not edit these files directly. +Instead, navigate to the appropriate typescript source file and edit the content there, then follow the build instructions to generate the markdown files. + +- Transformations + - Auto-generated markdown location: + - docs/sources/panels-visualizations/query-transform-data/transform-data/index.md + - Typescript location for editing and instructions: + - scripts/docs/generate-transformations.ts - Includes all content not specific to a transformation. + - public/app/features/transformers/docs/content.ts - Transformation-specific content. ### [Contributing](/contribute/documentation/README.md) diff --git a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md index a0895c8acdd..f1f242450ef 100644 --- a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md +++ b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md @@ -1,4 +1,16 @@ --- +comments: | + This Markdown file is auto-generated. Do not edit this file directly. + To build this Markdown, do the following: + + $ cd /docs (from the root of the repository) + $ make sources/panels-visualizations/query-transform-data/transform-data/index.md + $ make docs + + Browse to http://localhost:3003/docs/grafana/latest/panels-visualizations/query-transform-data/transform-data/ + + Refer to ./docs/README.md "Content guidelines" for more information about editing and building these docs. + aliases: - ../../panels/reference-transformation-functions/ - ../../panels/transform-data/ @@ -123,21 +135,21 @@ Use this transformation to add a new field calculated from two other fields. Eac - **Alias -** (Optional) Enter the name of your new field. If you leave this blank, then the field will be named to match the calculation. - **Replace all fields -** (Optional) Select this option if you want to hide all other fields and display only your calculated field in the visualization. -In the example below, I added two fields together and named them Sum. +In the example below, we added two fields together and named them Sum. {{< figure src="/static/img/docs/transformations/add-field-from-calc-stat-example-7-0.png" class="docs-image--no-shadow" max-width= "1100px" >}} ### Concatenate fields -This transformation combines all fields from all frames into one result. Consider: +Use this transformation to combine all fields from all frames into one result. Consider the following: -Query A: +**Query A:** | Temp | Uptime | | ---- | ------- | | 15.4 | 1230233 | -Query B: +**Query B:** | AQI | Errors | | --- | ------ | @@ -151,9 +163,9 @@ After you concatenate the fields, the data frame would be: ### Config from query results -This transformation allow you to select one query and from it extract standard options like **Min**, **Max**, **Unit** and **Thresholds** and apply it to other query results. This enables dynamic query driven visualization configuration. - -If you want to extract a unique config for every row in the config query result then try the rows to fields transformation. +Use this transformation to select one query and from it extract standard options such as +**Min**, **Max**, **Unit**, and **Thresholds** and apply them to other query results. +This enables dynamic query driven visualization configuration. #### Options @@ -161,9 +173,63 @@ If you want to extract a unique config for every row in the config query result - **Apply to**: Select what fields or series to apply the configuration to. - **Apply to options**: Usually a field type or field name regex depending on what option you selected in **Apply to**. +#### Field mapping table + +Below the configuration listed above you will find the field table. Here all fields found in the data returned by the config query will be listed along with a **Use as** and **Select** option. This table gives you control over what field should be mapped to which config property and if there are multiple rows which value to select. + +#### Example + +Input[0] (From query: A, name: ServerA) + +| Time | Value | +| ------------- | ----- | +| 1626178119127 | 10 | +| 1626178119129 | 30 | + +Input[1] (From query: B) + +| Time | Value | +| ------------- | ----- | +| 1626178119127 | 100 | +| 1626178119129 | 100 | + +Output (Same as Input[0] but now with config on the Value field) + +| Time | Value (config: Max=100) | +| ------------- | ----------------------- | +| 1626178119127 | 10 | +| 1626178119129 | 30 | + +Each row in the source data becomes a separate field. Each field now also has a maximum +configuration option set. Options such as **min**, **max**, **unit**, and **thresholds** are all part of field configuration, and if they are set like this, they will be used by the visualization instead of any options that are manually configured. +in the panel editor options pane. + +#### Value mappings + +You can also transform a query result into value mappings. This is is a bit different because every +row in the configuration query result is used to define a single value mapping row. See the following example. + +Config query result: + +| Value | Text | Color | +| ----- | ------ | ----- | +| L | Low | blue | +| M | Medium | green | +| H | High | red | + +In the field mapping specify: + +| Field | Use as | Select | +| ----- | ----------------------- | ---------- | +| Value | Value mappings / Value | All values | +| Text | Value mappings / Text | All values | +| Color | Value mappings / Ciolor | All values | + +Grafana will build the value mappings from you query result and apply it the the real data query results. You should see values being mapped and colored according to the config query results. + ### Convert field type -This transformation changes the field type of the specified field. +Use this transformation to change the field type of the specified field. - **Field -** Select from available fields - **as -** Select the FieldType to convert to @@ -173,7 +239,9 @@ This transformation changes the field type of the specified field. - Will show an option to specify a DateFormat as input by a string like yyyy-mm-dd or DD MM YYYY hh:mm:ss - **Boolean -** will make the values booleans -For example the following query could be modified by selecting the time field, as Time, and Date Format as YYYY. +For example, the following query could be modified by selecting the time field, as Time, and Date Format as YYYY. + +#### Sample Query | Time | Mark | Value | | ---------- | ----- | ----- | @@ -184,6 +252,8 @@ For example the following query could be modified by selecting the time field, a The result: +#### Transformed Query + | Time | Mark | Value | | ------------------- | ----- | ----- | | 2017-01-01 00:00:00 | above | 25 | @@ -191,29 +261,6 @@ The result: | 2019-01-01 00:00:00 | below | 29 | | 2020-01-01 00:00:00 | above | 22 | -### Create heatmap - -Use this transformation to prepare histogram data to be visualized over time. Similar to the [Heatmap panel][], this transformation allows you to convert histogram metrics to buckets over time. - -#### X Bucket - -This setting determines how the x-axis is split into buckets. - -- **Size** - Specify a time interval in the input field. For example, a time range of `1h` makes the cells one hour wide on the x-axis. -- **Count** - For non-time related series, use this option to define the number of elements in a bucket. - -#### Y Bucket - -This setting determines how the y-axis is split into buckets. - -#### Y Bucket scale - -Use this option to set the scale of the y-axes. Select from: - -- **Linear** -- **Logarithmic** - Use a base 2 or base 10. -- **Symlog** - A symmetrical logarithmic scale. Use a base 2 or base 10; allows negative values. - ### Extract fields Use this transformation to select one source of data and extract content from it in different formats. Set the following fields: @@ -221,13 +268,15 @@ Use this transformation to select one source of data and extract content from it - **Source** - Select the field for the source of data. - **Format** - Select one of the following: - **JSON** - To parse JSON content from the source. - - **Key+value parse** - To parse content in the format `a=b` or `c:d` from the source. + - **Key+value parse** - To parse content in the format 'a=b' or 'c:d' from the source. - **Auto** - To discover fields automatically. - **Replace all fields** - Optional: Select this option if you want to hide all other fields and display only your calculated field in the visualization. - **Keep time** - Optional: Only available if **Replace all fields** is true. Keep the time field in the output. Consider the following data set: +#### Data Set Example + | Timestamp | json_data | | ------------------- | ------------- | | 1636678740000000000 | {"value": 1} | @@ -239,19 +288,21 @@ You could prepare the data to be used by a [Time series panel][] with this confi - Source: json_data - Format: JSON - Field: value - - alias: my_value + - Alias: my_value - Replace all fields: true - Keep time: true This will generate the following output: +#### Transformed Data + | Timestamp | my_value | | ------------------- | -------- | | 1636678740000000000 | 1 | | 1636678680000000000 | 5 | | 1636678620000000000 | 12 | -### Field lookup +### Lookup fields from resource Use this transformation on a field value to look up additional fields from an external source. @@ -262,6 +313,8 @@ This transformation currently supports spatial data. For example, if you have this data: +#### Data Set Example + | Location | Values | | --------- | ------ | | AL | 0 | @@ -277,6 +330,8 @@ With this configuration: You'll get the following output: +#### Transformed Data + | Location | ID | Name | Lng | Lat | Values | | --------- | --- | -------- | ----------- | --------- | ------ | | AL | AL | Alabama | -80.891064 | 12.448457 | 0 | @@ -285,7 +340,84 @@ You'll get the following output: | Arkansas | | | | | 1 | | Somewhere | | | | | 5 | -### Filter by name +### Filter data by query refId + +Use this transformation in panels that have multiple queries, if you want to hide one or more of the queries. + +Grafana displays the query identification letters in dark gray text. Click a query identifier to toggle filtering. If the query letter is white, then the results are displayed. If the query letter is dark, then the results are hidden. + +> **Note:** This transformation is not available for Graphite because this data source does not support correlating returned data with queries. + +In the example below, the panel has three queries (A, B, C). We removed the B query from the visualization. + +{{< figure src="/static/img/docs/transformations/filter-by-query-stat-example-7-0.png" class="docs-image--no-shadow" max-width= "1100px" >}} + +### Filter data by values + +Use this transformation to filter your data directly in Grafana and remove some data points from your query result. You have the option to include or exclude data that match one or more conditions you define. The conditions are applied on a selected field. + +This transformation is very useful if your data source does not natively filter by values. You might also use this to narrow values to display if you are using a shared query. + +The available conditions for all fields are: + +- **Regex:** Match a regex expression +- **Is Null:** Match if the value is null +- **Is Not Null:** Match if the value is not null +- **Equal:** Match if the value is equal to the specified value +- **Different:** Match if the value is different than the specified value + +The available conditions for number fields are: + +- **Greater:** Match if the value is greater than the specified value +- **Lower:** Match if the value is lower than the specified value +- **Greater or equal:** Match if the value is greater or equal +- **Lower or equal:** Match if the value is lower or equal +- **Range:** Match a range between a specified minimum and maximum, min and max included + +Consider the following data set: + +#### Data Set Example + +| Time | Temperature | Altitude | +| ------------------- | ----------- | -------- | +| 2020-07-07 11:34:23 | 32 | 101 | +| 2020-07-07 11:34:22 | 28 | 125 | +| 2020-07-07 11:34:21 | 26 | 110 | +| 2020-07-07 11:34:20 | 23 | 98 | +| 2020-07-07 10:32:24 | 31 | 95 | +| 2020-07-07 10:31:22 | 20 | 85 | +| 2020-07-07 09:30:57 | 19 | 101 | + +If you **Include** the data points that have a temperature below 30°C, the configuration will look as follows: + +- Filter Type: 'Include' +- Condition: Rows where 'Temperature' matches 'Lower Than' '30' + +And you will get the following result, where only the temperatures below 30°C are included: + +#### Transformed Data + +| Time | Temperature | Altitude | +| ------------------- | ----------- | -------- | +| 2020-07-07 11:34:22 | 28 | 125 | +| 2020-07-07 11:34:21 | 26 | 110 | +| 2020-07-07 11:34:20 | 23 | 98 | +| 2020-07-07 10:31:22 | 20 | 85 | +| 2020-07-07 09:30:57 | 19 | 101 | + +You can add more than one condition to the filter. For example, you might want to include the data only if the altitude is greater than 100. To do so, add that condition to the following configuration: + +- Filter type: 'Include' rows that 'Match All' conditions +- Condition 1: Rows where 'Temperature' matches 'Lower' than '30' +- Condition 2: Rows where 'Altitude' matches 'Greater' than '100' + +When you have more than one condition, you can choose if you want the action (include / exclude) to be applied on rows that **Match all** conditions or **Match any** of the conditions you added. + +In the example above, we chose **Match all** because we wanted to include the rows that have a temperature lower than 30°C _AND_ an altitude higher than 100. If we wanted to include the rows that have a temperature lower than 30°C _OR_ an altitude higher than 100 instead, then we would select **Match any**. This would include the first row in the original data, which has a temperature of 32°C (does not match the first condition) but an altitude of 101 (which matches the second condition), so it is included. + +Conditions that are invalid or incompletely configured are ignored. + +### Filter fields by name Use this transformation to remove parts of the query results. @@ -306,14 +438,14 @@ From the input data: | 2023-03-04 23:56:23 | 23.5 | 24.5 | 22.2 | 20.2 | | 2023-03-04 23:56:23 | 23.6 | 24.4 | 22.1 | 20.1 | -The result from using the regular expression `prod.*` would be: +The result from using the regular expression 'prod.\*' would be: | Time | prod-eu-west | prod-eu-north | | ------------------- | ------------ | ------------- | | 2023-03-04 23:56:23 | 22.2 | 20.2 | | 2023-03-04 23:56:23 | 22.1 | 20.1 | -The regular expression can include an interpolated dashboard variable by using the `${[variable name]}` syntax. +The regular expression can include an interpolated dashboard variable by using the ${$variableName} syntax. #### Manually select included fields @@ -321,86 +453,44 @@ Click and uncheck the field names to remove them from the result. Fields that ar #### Use a dashboard variable -Enable `From variable` to let you select a dashboard variable that's used to include fields. By setting up a [dashboard variable][] with multiple choices, the same fields can be displayed across multiple visualizations. +Enable 'From variable' to let you select a dashboard variable that's used to include fields. By setting up a [dashboard variable][] with multiple choices, the same fields can be displayed across multiple visualizations. -### Filter data by query +{{< figure src="/static/img/docs/transformations/filter-name-table-before-7-0.png" class="docs-image--no-shadow" max-width= "1100px" >}} -Use this transformation in panels that have multiple queries, if you want to hide one or more of the queries. +Here's the table after we applied the transformation to remove the Min field. -Grafana displays the query identification letters in dark gray text. Click a query identifier to toggle filtering. If the query letter is white, then the results are displayed. If the query letter is dark, then the results are hidden. +{{< figure src="/static/img/docs/transformations/filter-name-table-after-7-0.png" class="docs-image--no-shadow" max-width= "1100px" >}} -In the example below, the panel has three queries (A, B, C). I removed the B query from the visualization. +Here is the same query using a Stat visualization. -{{< figure src="/static/img/docs/transformations/filter-by-query-stat-example-7-0.png" class="docs-image--no-shadow" max-width= "1100px" >}} +{{< figure src="/static/img/docs/transformations/filter-name-stat-after-7-0.png" class="docs-image--no-shadow" max-width= "1100px" >}} -{{% admonition type="note" %}} -This transformation is not available for Graphite because this data source does not support correlating returned data with queries. -{{% /admonition %}} +### Format string -### Filter data by value +> **Note:** This transformation is an experimental feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. Enable the 'formatString' in Grafana to use this feature. Contact Grafana Support to enable this feature in Grafana Cloud. -This transformation allows you to filter your data directly in Grafana and remove some data points from your query result. You have the option to include or exclude data that match one or more conditions you define. The conditions are applied on a selected field. +Use this transformation to format the output of a string field. You can format output in the following ways: -This transformation is very useful if your data source does not natively filter by values. You might also use this to narrow values to display if you are using a shared query. +- Upper case - Formats the entire string in upper case characters. +- Lower case - Formats the entire string in lower case characters. +- Sentence case - Formats the the first character of the string in upper case. +- Title case - Formats the first character of each word in the string in upper case. +- Pascal case - Formats the first character of each word in the string in upper case and doesn't include spaces between words. +- Camel case - Formats the first character of each word in the string in upper case, except the first word, and doesn't include spaces between words. +- Snake case - Formats all characters in the string in lower case and uses underscores instead of spaces between words. +- Kebab case - Formats all characters in the string in lower case and uses dashes instead of spaces between words. +- Trim - Removes all leading and trailing spaces from the string. +- Substring - Returns a substring of the string, using the specified start and end positions. -The available conditions for all fields are: +### Format time -- **Regex:** Match a regex expression -- **Is Null:** Match if the value is null -- **Is Not Null:** Match if the value is not null -- **Equal:** Match if the value is equal to the specified value -- **Different:** match if the value is different than the specified value +Use this transformation to format the output of a time field. Output can be formatted using [Moment.js format strings](https://momentjs.com/docs/#/displaying/). For instance, if you would like to display only the year of a time field the format string 'YYYY' can be used to show the calendar year (e.g. 1999, 2012, etc.). -The available conditions for number fields are: - -- **Greater:** Match if the value is greater than the specified value -- **Lower:** Match if the value is lower than the specified value -- **Greater or equal:** Match if the value is greater or equal -- **Lower or equal:** Match if the value is lower or equal -- **Range:** Match a range between a specified minimum and maximum, min and max included - -Consider the following data set: - -| Time | Temperature | Altitude | -| ------------------- | ----------- | -------- | -| 2020-07-07 11:34:23 | 32 | 101 | -| 2020-07-07 11:34:22 | 28 | 125 | -| 2020-07-07 11:34:21 | 26 | 110 | -| 2020-07-07 11:34:20 | 23 | 98 | -| 2020-07-07 10:32:24 | 31 | 95 | -| 2020-07-07 10:31:22 | 20 | 85 | -| 2020-07-07 09:30:57 | 19 | 101 | - -If you **Include** the data points that have a temperature below 30°C, the configuration will look as follows: - -- Filter Type: `Include` -- Condition: Rows where `Temperature` matches `Lower Than` `30` - -And you will get the following result, where only the temperatures below 30°C are included: - -| Time | Temperature | Altitude | -| ------------------- | ----------- | -------- | -| 2020-07-07 11:34:22 | 28 | 125 | -| 2020-07-07 11:34:21 | 26 | 110 | -| 2020-07-07 11:34:20 | 23 | 98 | -| 2020-07-07 10:31:22 | 20 | 85 | -| 2020-07-07 09:30:57 | 19 | 101 | - -You can add more than one condition to the filter. For example, you might want to include the data only if the altitude is greater than 100. To do so, add that condition to the following configuration: - -- Filter type: `Include` rows that `Match All` conditions -- Condition 1: Rows where `Temperature` matches `Lower` than `30` -- Condition 2: Rows where `Altitude` matches `Greater` than `100` - -When you have more than one condition, you can choose if you want the action (include / exclude) to be applied on rows that **Match all** conditions or **Match any** of the conditions you added. - -In the example above we chose **Match all** because we wanted to include the rows that have a temperature lower than 30 _AND_ an altitude higher than 100. If we wanted to include the rows that have a temperature lower than 30 _OR_ an altitude higher than 100 instead, then we would select **Match any**. This would include the first row in the original data, which has a temperature of 32°C (does not match the first condition) but an altitude of 101 (which matches the second condition), so it is included. - -Conditions that are invalid or incompletely configured are ignored. +> **Note:** This transformation is available in Grafana 10.1+ as an alpha feature. ### Group by -This transformation groups the data by a specified field (column) value and processes calculations on each group. Click to see a list of calculation choices. For information about available calculations, refer to [Calculation types][]. +Use this transformation to group the data by a specified field (column) value and process calculations on each group. Click to see a list of calculation choices. For information about available calculations, refer to [Calculation types][]. Here's an example of original data. @@ -468,15 +558,34 @@ Use this transformation to combine three fields-that will be used as input for t | server 2 | 88.6 | OK | | server 3 | 59.6 | Shutdown | -We can generate a matrix using the values of `Server Status` as column names, the `Server ID` values as row names, and the `CPU Temperature` as content of each cell. The content of each cell will appear for the existing column (`Server Status`) and row combination (`Server ID`). For the rest of the cells, you can select which value to display between: **Null**, **True**, **False**, or **Empty**. +We can generate a matrix using the values of 'Server Status' as column names, the 'Server ID' values as row names, and the 'CPU Temperature' as content of each cell. The content of each cell will appear for the existing column ('Server Status') and row combination ('Server ID'). For the rest of the cells, you can select which value to display between: **Null**, **True**, **False**, or **Empty**. **Output** -| Server ID\Server Status | OK | Shutdown | -| ----------------------- | ---- | -------- | -| server 1 | 82 | | -| server 2 | 88.6 | | -| server 3 | | 59.6 | +| Server IDServer Status | OK | Shutdown | +| ---------------------- | ---- | -------- | +| server 1 | 82 | | +| server 2 | 88.6 | | +| server 3 | | 59.6 | + +### Create heatmap + +Use this transformation to prepare histogram data to be visualized over time. Similar to the Heatmap panel, this transformation allows you to convert histogram metrics to buckets over time. + +#### X Bucket + +This setting determines how the x-axis is split into buckets. + +- **Size** - Specify a time interval in the input field. For example, a time range of '1h' makes the cells one hour wide on the x-axis. +- **Count** - For non-time related series, use this option to define the number of elements in a bucket. + +#### Y Bucket + +This setting determines how the y-axis is split into buckets. + +- **Linear** +- **Logarithmic** - Use a base 2 or base 10. +- **Symlog** - A symmetrical logarithmic scale. Use a base 2 or base 10; allows negative values. ### Histogram @@ -561,7 +670,7 @@ The result after applying the inner join transformation looks like the following #### Outer join -An outer join includes all data from an inner join and rows where values do not match in every input. While the inner join joins Query A and Query B on the time field, the outer join includes all rows that don’t match on the time field. +An outer join includes all data from an inner join and rows where values do not match in every input. While the inner join joins Query A and Query B on the time field, the outer join includes all rows that don't match on the time field. In the following example, two queries return table data. It is visualized as two tables before applying the outer join transformation. @@ -610,21 +719,21 @@ time series results into a single wide table with a shared **Label** field. ##### Input -serie1{what="Temp", cluster="A", job="J1"} +series1{what="Temp", cluster="A", job="J1"} | Time | Value | | ---- | ----- | | 1 | 10 | | 2 | 200 | -serie2{what="Temp", cluster="B", job="J1"} +series2{what="Temp", cluster="B", job="J1"} | Time | Value | | ---- | ----- | | 1 | 10 | | 2 | 200 | -serie3{what="Speed", cluster="B", job="J1"} +series3{what="Speed", cluster="B", job="J1"} | Time | Value | | ---- | ----- | @@ -646,7 +755,7 @@ value: "what" ### Labels to fields -This transformation changes time series results that include labels or tags into a table where each label keys and values are included in the table result. The labels can be displayed either as columns or as row values. +Use this transformation to change time series results that include labels or tags into a table where each label's keys and values are included in the table result. The labels can be displayed as either columns or row values. Given a query result of two time series: @@ -706,6 +815,29 @@ After merge: | 2020-07-07 11:34:20 | ServerA | 10 | | | 2020-07-07 11:34:20 | | 20 | EU | +### Limit + +Use this transformation to limit the number of rows displayed. + +In the example below, we have the following response from the data source: + +| Time | Metric | Value | +| ------------------- | ----------- | ----- | +| 2020-07-07 11:34:20 | Temperature | 25 | +| 2020-07-07 11:34:20 | Humidity | 22 | +| 2020-07-07 10:32:20 | Humidity | 29 | +| 2020-07-07 10:31:22 | Temperature | 22 | +| 2020-07-07 09:30:57 | Humidity | 33 | +| 2020-07-07 09:30:05 | Temperature | 19 | + +Here is the result after adding a Limit transformation with a value of '3': + +| Time | Metric | Value | +| ------------------- | ----------- | ----- | +| 2020-07-07 11:34:20 | Temperature | 25 | +| 2020-07-07 11:34:20 | Humidity | 22 | +| 2020-07-07 10:32:20 | Humidity | 29 | + ### Merge Use this transformation to combine the result from multiple queries into one single result. This is helpful when using the table panel visualization. Values that can be merged are combined into the same row. Values are mergeable if the shared fields contain the same data. For information, refer to [Table panel][]. @@ -733,13 +865,11 @@ Here is the result after applying the Merge transformation. | 2020-07-07 11:34:20 | node | 15 | 25260122 | | 2020-07-07 11:24:20 | postgre | 5 | 123001233 | -### Organize fields +### Oraganize fields Use this transformation to rename, reorder, or hide fields returned by the query. -{{% admonition type="note" %}} -This transformation only works in panels with a single query. If your panel has multiple queries, then you must either apply an Outer join transformation or remove the extra queries. -{{% /admonition %}} +> **Note:** This transformation only works in panels with a single query. If your panel has multiple queries, then you must either apply an Outer join transformation or remove the extra queries. Grafana displays a list of fields returned by the query. You can: @@ -747,13 +877,9 @@ Grafana displays a list of fields returned by the query. You can: - Hide or show a field by clicking the eye icon next to the field name. - Rename fields by typing a new name in the **Rename ** box. -In the example below, I hid the value field and renamed Max and Min. - -{{< figure src="/static/img/docs/transformations/organize-fields-stat-example-7-0.png" class="docs-image--no-shadow" max-width= "1100px" >}} - ### Partition by values -This transformation can help eliminate the need for multiple queries to the same datasource with different `WHERE` clauses when graphing multiple series. Consider a metrics SQL table with the following data: +Use this transformation to eliminate the need for multiple queries to the same data source with different 'WHERE' clauses when graphing multiple series. Consider a metrics SQL table with the following data: | Time | Region | Value | | ------------------- | ------ | ----- | @@ -764,14 +890,14 @@ This transformation can help eliminate the need for multiple queries to the same Prior to v9.3, if you wanted to plot a red trendline for US and a blue one for EU in the same TimeSeries panel, you would likely have to split this into two queries: -`SELECT Time, Value FROM metrics WHERE Time > '2022-10-20' AND Region='US'`
-`SELECT Time, Value FROM metrics WHERE Time > '2022-10-20' AND Region='EU'` +'SELECT Time, Value FROM metrics WHERE Time > "2022-10-20" AND Region="US"'
+'SELECT Time, Value FROM metrics WHERE Time > "2022-10-20" AND Region="EU"' This also requires you to know ahead of time which regions actually exist in the metrics table. -With the _Partition by values_ transformer, you can now issue a single query and split the results by unique values in one or more columns (`fields`) of your choosing. The following example uses `Region`. +With the _Partition by values_ transformer, you can now issue a single query and split the results by unique values in one or more columns ('fields') of your choosing. The following example uses 'Region'. -`SELECT Time, Region, Value FROM metrics WHERE Time > '2022-10-20'` +'SELECT Time, Region, Value FROM metrics WHERE Time > "2022-10-20"' | Time | Region | Value | | ------------------- | ------ | ----- | @@ -783,14 +909,21 @@ With the _Partition by values_ transformer, you can now issue a single query and | 2022-10-20 12:00:00 | EU | 2936 | | 2022-10-20 01:00:00 | EU | 912 | -There are two naming modes: +### Prepare time series -- **As labels** - The value that results are partitioned by is set as a label. -- **As frame name** - The value is used to set the frame name. This is useful if the data will be visualized in a table. +Use this transformation when a data source returns time series data in a format that isn't supported by the panel you want to use. For more information about data frame formats, refer to [Data frames][]. + +This transformation helps you resolve this issue by converting the time series data from either the wide format to the long format or the other way around. + +Select the 'Multi-frame time series' option to transform the time series data frame from the wide to the long format. + +Select the 'Wide time series' option to transform the time series data frame from the long to the wide format. + +> **Note:** This transformation is available in Grafana 7.5.10+ and Grafana 8.0.6+. ### Reduce -The _Reduce_ transformation applies a calculation to each field in the frame and return a single value. Time fields are removed when applying this transformation. +Use this transformation to apply a calculation to each field in the frame and return a single value. Time fields are removed when applying this transformation. Consider the input: @@ -842,9 +975,9 @@ Query B: Use this transformation to rename parts of the query results using a regular expression and replacement pattern. -You can specify a regular expression, which is only applied to matches, along with a replacement pattern that support back references. For example, let's imagine you're visualizing CPU usage per host and you want to remove the domain name. You could set the regex to `([^\.]+)\..+` and the replacement pattern to `$1`, `web-01.example.com` would become `web-01`. +You can specify a regular expression, which is only applied to matches, along with a replacement pattern that support back references. For example, let's imagine you're visualizing CPU usage per host and you want to remove the domain name. You could set the regex to '([^.]+)..+' and the replacement pattern to '$1', 'web-01.example.com' would become 'web-01'. -In the following example, we are stripping the prefix from event types. In the before image, you can see everything is prefixed with `system.` +In the following example, we are stripping the prefix from event types. In the before image, you can see everything is prefixed with 'system.' {{< figure src="/static/img/docs/transformations/rename-by-regex-before-7-3.png" class="docs-image--no-shadow" max-width= "1100px" >}} @@ -854,7 +987,7 @@ With the transformation applied, you can see we are left with just the remainder ### Rows to fields -The rows to fields transformation converts rows into separate fields. This can be useful as fields can be styled and configured individually. It can also use additional fields as sources for dynamic field configuration or map them to field labels. The additional labels can then be used to define better display names for the resulting fields. +Use this transformation to convert rows into separate fields. This can be useful because fields can be styled and configured individually. It can also use additional fields as sources for dynamic field configuration or map them to field labels. The additional labels can then be used to define better display names for the resulting fields. This transformation includes a field table which lists all fields in the data returned by the config query. This table gives you control over what field should be mapped to each config property (the \*Use as\*\* option). You can also choose which value to select if there are multiple rows in the returned data. @@ -913,26 +1046,8 @@ Output: As you can see each row in the source data becomes a separate field. Each field now also has a max config option set. Options like **Min**, **Max**, **Unit** and **Thresholds** are all part of field configuration and if set like this will be used by the visualization instead of any options manually configured in the panel editor options pane. -### Prepare time series - -{{% admonition type="note" %}} -This transformation is available in Grafana 7.5.10+ and Grafana 8.0.6+. -{{% /admonition %}} - -Prepare time series transformation is useful when a data source returns time series data in a format that isn't supported by the panel you want to use. For more information about data frame formats, refer to [Data frames](https://grafana.com/developers/plugin-tools/introduction/data-frames). - -This transformation helps you resolve this issue by converting the time series data from either the wide format to the long format or the other way around. - -Select the `Multi-frame time series` option to transform the time series data frame from the wide to the long format. - -Select the `Wide time series` option to transform the time series data frame from the long to the wide format. - ### Series to rows -{{% admonition type="note" %}} -This transformation is available in Grafana 7.1+. -{{% /admonition %}} - Use this transformation to combine the result from multiple time series data queries into one single result. This is helpful when using the table panel visualization. The result from this transformation will contain three columns: Time, Metric, and Value. The Metric column is added so you easily can see from which query the metric originates from. Customize this value by defining Label on the source query. @@ -966,32 +1081,15 @@ Here is the result after applying the Series to rows transformation. | 2020-07-07 09:30:57 | Humidity | 33 | | 2020-07-07 09:30:05 | Temperature | 19 | +> **Note:** This transformation is available in Grafana 7.1+. + ### Sort by -This transformation will sort each frame by the configured field, When `reverse` is checked, the values will return in the opposite order. +Use this transformation to sort each frame by the configured field. When the **Reverse** switch is on, the values will return in the opposite order. -### Limit +### Spatial -Use this transformation to limit the number of rows displayed. - -In the example below, we have the following response from the data source: - -| Time | Metric | Value | -| ------------------- | ----------- | ----- | -| 2020-07-07 11:34:20 | Temperature | 25 | -| 2020-07-07 11:34:20 | Humidity | 22 | -| 2020-07-07 10:32:20 | Humidity | 29 | -| 2020-07-07 10:31:22 | Temperature | 22 | -| 2020-07-07 09:30:57 | Humidity | 33 | -| 2020-07-07 09:30:05 | Temperature | 19 | - -Here is the result after adding a Limit transformation with a value of '3': - -| Time | Metric | Value | -| ------------------- | ----------- | ----- | -| 2020-07-07 11:34:20 | Temperature | 25 | -| 2020-07-07 11:34:20 | Humidity | 22 | -| 2020-07-07 10:32:20 | Humidity | 29 | +Use this transformation to apply spatial operations to query results ### Time series to table transform @@ -999,35 +1097,15 @@ Use this transformation to convert time series result into a table, converting t For each generated "Trend" field value calculation function can be selected. Default is "last non null value". This value will be displayed next to the sparkline and used for sorting table rows. -### Format Time - -{{% admonition type="note" %}} -This transformation is available in Grafana 10.1+ as an alpha feature. -{{% /admonition %}} - -Use this transformation to format the output of a time field. Output can be formatted using (Moment.js format strings)[https://momentjs.com/docs/#/displaying/]. For instance, if you would like to display only the year of a time field the format string `YYYY` can be used to show the calendar year (e.g. 1999, 2012, etc.). - -### Format string - -> **Note:** This transformation is an experimental feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. Enable the `formatString` in Grafana to use this feature. Contact Grafana Support to enable this feature in Grafana Cloud. - -Use this transformation to format the output of a string field. You can format output in the following ways: - -- Upper case - Formats the entire string in upper case characters. -- Lower case - Formats the entire string in lower case characters. -- Sentence case - Formats the the first character of the string in upper case. -- Title case - Formats the first character of each word in the string in upper case. -- Pascal case - Formats the first character of each word in the string in upper case and doesn't include spaces between words. -- Camel case - Formats the first character of each word in the string in upper case, except the first word, and doesn't include spaces between words. -- Snake case - Formats all characters in the string in lower case and uses underscores instead of spaces between words. -- Kebab case - Formats all characters in the string in lower case and uses dashes instead of spaces between words. -- Trim - Removes all leading and trailing spaces from the string. -- Substring - Returns a substring of the string, using the specified start and end positions. +> **Note:** This transformation is available in Grafana 9.5+ as an opt-in beta feature. Modify Grafana [configuration file][] to use it. {{% docs/reference %}} [Table panel]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/table" [Table panel]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/visualizations/table" +[Data frames]: "/docs/grafana/ -> /docs/grafana//developers/plugins/introduction-to-plugin-development/data-frames" +[Data frames]: "/docs/grafana-cloud/ -> /docs/grafana//developers/plugins/introduction-to-plugin-development/data-frames" + [Calculation types]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/calculation-types" [Calculation types]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/calculation-types" diff --git a/public/app/features/transformers/docs/content.ts b/public/app/features/transformers/docs/content.ts new file mode 100644 index 00000000000..19ffdb3b1ae --- /dev/null +++ b/public/app/features/transformers/docs/content.ts @@ -0,0 +1,1224 @@ +/* + NOTE: This file is used to generate the transformation docs markdown content. If you change/update the content here, + please then rebuild the markdown by doing the following: + + $ cd /docs (from the root of the repository) + $ make sources/panels-visualizations/query-transform-data/transform-data/index.md + $ make docs + + Browse to http://localhost:3003/docs/grafana/latest/panels-visualizations/query-transform-data/transform-data/ + + Refer to ./docs/README.md for more information about building docs. +*/ + +interface Link { + title: string; + url: string; +} +export interface TransformationInfo { + name: string; + getHelperDocs: (imageRenderType?: ImageRenderType) => string; + links?: Link[]; +} + +export enum ImageRenderType { + ShortcodeFigure = 'shortcodeFigure', + UIImage = 'uiImage', +} + +export interface TransformationDocsContentType { + [key: string]: TransformationInfo; +} + +export const transformationDocsContent: TransformationDocsContentType = { + calculateField: { + name: 'Add field from calculation', + /* + `getHelperDocs` will build the markdown content based in the `ImageRenderType`. + The images will either be rendered in Hugo Shortcode format or as standard markdown for UI usage. + */ + getHelperDocs: function (imageRenderType: ImageRenderType = ImageRenderType.ShortcodeFigure) { + return ` + Use this transformation to add a new field calculated from two other fields. Each transformation allows you to add one new field. + + - **Mode -** Select a mode: + - **Reduce row -** Apply selected calculation on each row of selected fields independently. + - **Binary operation -** Apply basic binary operations (for example, sum or multiply) on values in a single row from two selected fields. + - **Unary operation -** Apply basic unary operations on values in a single row from a selected field. The available operations are: + - **Absolute value (abs)** - Returns the absolute value of a given expression. It represents its distance from zero as a positive number. + - **Natural exponential (exp)** - Returns _e_ raised to the power of a given expression. + - **Natural logarithm (ln)** - Returns the natural logarithm of a given expression. + - **Floor (floor)** - Returns the largest integer less than or equal to a given expression. + - **Ceiling (ceil)** - Returns the smallest integer greater than or equal to a given expression. + - **Row index -** Insert a field with the row index. + - **Field name -** Select the names of fields you want to use in the calculation for the new field. + - **Calculation -** If you select **Reduce row** mode, then the **Calculation** field appears. Click in the field to see a list of calculation choices you can use to create the new field. For information about available calculations, refer to [Calculation types][]. + - **Operation -** If you select **Binary operation** or **Unary operation** mode, then the **Operation** fields appear. These fields allow you to apply basic math operations on values in a single row from selected fields. You can also use numerical values for binary operations. + - **As percentile -** If you select **Row index** mode, then the **As percentile** switch appears. This switch allows you to transform the row index as a percentage of the total number of rows. + - **Alias -** (Optional) Enter the name of your new field. If you leave this blank, then the field will be named to match the calculation. + - **Replace all fields -** (Optional) Select this option if you want to hide all other fields and display only your calculated field in the visualization. + + In the example below, we added two fields together and named them Sum. + + ${buildImageContent( + '/static/img/docs/transformations/add-field-from-calc-stat-example-7-0.png', + imageRenderType, + this.name + )} + `; + }, + links: [ + { + title: 'Calculation types', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/calculation-types/', + }, + ], + }, + concatenate: { + name: 'Concatenate fields', + getHelperDocs: function () { + return ` + Use this transformation to combine all fields from all frames into one result. Consider the following: + + **Query A:** + + | Temp | Uptime | + | ----- | --------- | + | 15.4 | 1230233 | + + **Query B:** + + | AQI | Errors | + | ----- | ------ | + | 3.2 | 5 | + + After you concatenate the fields, the data frame would be: + + | Temp | Uptime | AQI | Errors | + | ----- | -------- | ----- | ------ | + | 15.4 | 1230233 | 3.2 | 5 | + `; + }, + }, + configFromData: { + name: 'Config from query results', + getHelperDocs: function () { + return ` + Use this transformation to select one query and from it extract standard options such as + **Min**, **Max**, **Unit**, and **Thresholds** and apply them to other query results. + This enables dynamic query driven visualization configuration. + + #### Options + + - **Config query**: Select the query that returns the data you want to use as configuration. + - **Apply to**: Select what fields or series to apply the configuration to. + - **Apply to options**: Usually a field type or field name regex depending on what option you selected in **Apply to**. + + #### Field mapping table + + Below the configuration listed above you will find the field table. Here all fields found in the data returned by the config query will be listed along with a **Use as** and **Select** option. This table gives you control over what field should be mapped to which config property and if there are multiple rows which value to select. + + #### Example + + Input[0] (From query: A, name: ServerA) + + | Time | Value | + | ------------- | ----- | + | 1626178119127 | 10 | + | 1626178119129 | 30 | + + Input[1] (From query: B) + + | Time | Value | + | ------------- | ----- | + | 1626178119127 | 100 | + | 1626178119129 | 100 | + + Output (Same as Input[0] but now with config on the Value field) + + | Time | Value (config: Max=100) | + | ------------- | ----------------------- | + | 1626178119127 | 10 | + | 1626178119129 | 30 | + + Each row in the source data becomes a separate field. Each field now also has a maximum + configuration option set. Options such as **min**, **max**, **unit**, and **thresholds** are all part of field configuration, and if they are set like this, they will be used by the visualization instead of any options that are manually configured. + in the panel editor options pane. + + #### Value mappings + + You can also transform a query result into value mappings. This is is a bit different because every + row in the configuration query result is used to define a single value mapping row. See the following example. + + Config query result: + + | Value | Text | Color | + | ----- | ------ | ----- | + | L | Low | blue | + | M | Medium | green | + | H | High | red | + + In the field mapping specify: + + | Field | Use as | Select | + | ----- | ----------------------- | ---------- | + | Value | Value mappings / Value | All values | + | Text | Value mappings / Text | All values | + | Color | Value mappings / Ciolor | All values | + + Grafana will build the value mappings from you query result and apply it the the real data query results. You should see values being mapped and colored according to the config query results. + `; + }, + }, + convertFieldType: { + name: 'Convert field type', + getHelperDocs: function () { + return ` + Use this transformation to change the field type of the specified field. + + - **Field -** Select from available fields + - **as -** Select the FieldType to convert to + - **Numeric -** attempts to make the values numbers + - **String -** will make the values strings + - **Time -** attempts to parse the values as time + - Will show an option to specify a DateFormat as input by a string like yyyy-mm-dd or DD MM YYYY hh:mm:ss + - **Boolean -** will make the values booleans + + For example, the following query could be modified by selecting the time field, as Time, and Date Format as YYYY. + + #### Sample Query + + | Time | Mark | Value | + |------------|-----------|-------| + | 2017-07-01 | above | 25 | + | 2018-08-02 | below | 22 | + | 2019-09-02 | below | 29 | + | 2020-10-04 | above | 22 | + + The result: + + #### Transformed Query + + | Time | Mark | Value | + |---------------------|-----------|-------| + | 2017-01-01 00:00:00 | above | 25 | + | 2018-01-01 00:00:00 | below | 22 | + | 2019-01-01 00:00:00 | below | 29 | + | 2020-01-01 00:00:00 | above | 22 | + `; + }, + }, + extractFields: { + name: 'Extract fields', + getHelperDocs: function () { + return ` + Use this transformation to select one source of data and extract content from it in different formats. Set the following fields: + + - **Source** - Select the field for the source of data. + - **Format** - Select one of the following: + - **JSON** - To parse JSON content from the source. + - **Key+value parse** - To parse content in the format 'a=b' or 'c:d' from the source. + - **Auto** - To discover fields automatically. + - **Replace all fields** - Optional: Select this option if you want to hide all other fields and display only your calculated field in the visualization. + - **Keep time** - Optional: Only available if **Replace all fields** is true. Keep the time field in the output. + + Consider the following data set: + + #### Data Set Example + + | Timestamp | json_data | + |-------------------|-----------| + | 1636678740000000000 | {"value": 1} | + | 1636678680000000000 | {"value": 5} | + | 1636678620000000000 | {"value": 12} | + + You could prepare the data to be used by a [Time series panel][] with this configuration: + + - Source: json_data + - Format: JSON + - Field: value + - Alias: my_value + - Replace all fields: true + - Keep time: true + + This will generate the following output: + + #### Transformed Data + + | Timestamp | my_value | + |-------------------|----------| + | 1636678740000000000 | 1 | + | 1636678680000000000 | 5 | + | 1636678620000000000 | 12 | + `; + }, + links: [ + { + title: 'Time series panel', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/time-series/', + }, + ], + }, + fieldLookup: { + name: 'Lookup fields from resource', + getHelperDocs: function () { + return ` + Use this transformation on a field value to look up additional fields from an external source. + + - **Field** - Select a text field. + - **Lookup** - Select from **Countries**, **USA States**, and **Airports**. + + This transformation currently supports spatial data. + + For example, if you have this data: + + #### Data Set Example + + | Location | Values | + |-----------|--------| + | AL | 0 | + | AK | 10 | + | Arizona | 5 | + | Arkansas | 1 | + | Somewhere | 5 | + + With this configuration: + + - Field: location + - Lookup: USA States + + You'll get the following output: + + #### Transformed Data + + | Location | ID | Name | Lng | Lat | Values | + |-----------|----|-----------|------------|------------|--------| + | AL | AL | Alabama | -80.891064 | 12.448457 | 0 | + | AK | AK | Arkansas | -100.891064| 24.448457 | 10 | + | Arizona | | | | | 5 | + | Arkansas | | | | | 1 | + | Somewhere | | | | | 5 | + `; + }, + }, + filterByRefId: { + name: 'Filter data by query refId', + getHelperDocs: function (imageRenderType: ImageRenderType = ImageRenderType.ShortcodeFigure) { + return ` + Use this transformation in panels that have multiple queries, if you want to hide one or more of the queries. + + Grafana displays the query identification letters in dark gray text. Click a query identifier to toggle filtering. If the query letter is white, then the results are displayed. If the query letter is dark, then the results are hidden. + + > **Note:** This transformation is not available for Graphite because this data source does not support correlating returned data with queries. + + In the example below, the panel has three queries (A, B, C). We removed the B query from the visualization. + + ${buildImageContent( + '/static/img/docs/transformations/filter-by-query-stat-example-7-0.png', + imageRenderType, + this.name + )} + `; + }, + }, + filterByValue: { + name: 'Filter data by values', + getHelperDocs: function () { + return ` + Use this transformation to filter your data directly in Grafana and remove some data points from your query result. You have the option to include or exclude data that match one or more conditions you define. The conditions are applied on a selected field. + + This transformation is very useful if your data source does not natively filter by values. You might also use this to narrow values to display if you are using a shared query. + + The available conditions for all fields are: + + - **Regex:** Match a regex expression + - **Is Null:** Match if the value is null + - **Is Not Null:** Match if the value is not null + - **Equal:** Match if the value is equal to the specified value + - **Different:** Match if the value is different than the specified value + + The available conditions for number fields are: + + - **Greater:** Match if the value is greater than the specified value + - **Lower:** Match if the value is lower than the specified value + - **Greater or equal:** Match if the value is greater or equal + - **Lower or equal:** Match if the value is lower or equal + - **Range:** Match a range between a specified minimum and maximum, min and max included + + Consider the following data set: + + #### Data Set Example + + | Time | Temperature | Altitude | + |---------------------|-------------|----------| + | 2020-07-07 11:34:23 | 32 | 101 | + | 2020-07-07 11:34:22 | 28 | 125 | + | 2020-07-07 11:34:21 | 26 | 110 | + | 2020-07-07 11:34:20 | 23 | 98 | + | 2020-07-07 10:32:24 | 31 | 95 | + | 2020-07-07 10:31:22 | 20 | 85 | + | 2020-07-07 09:30:57 | 19 | 101 | + + If you **Include** the data points that have a temperature below 30°C, the configuration will look as follows: + + - Filter Type: 'Include' + - Condition: Rows where 'Temperature' matches 'Lower Than' '30' + + And you will get the following result, where only the temperatures below 30°C are included: + + #### Transformed Data + + | Time | Temperature | Altitude | + |---------------------|-------------|----------| + | 2020-07-07 11:34:22 | 28 | 125 | + | 2020-07-07 11:34:21 | 26 | 110 | + | 2020-07-07 11:34:20 | 23 | 98 | + | 2020-07-07 10:31:22 | 20 | 85 | + | 2020-07-07 09:30:57 | 19 | 101 | + + You can add more than one condition to the filter. For example, you might want to include the data only if the altitude is greater than 100. To do so, add that condition to the following configuration: + + - Filter type: 'Include' rows that 'Match All' conditions + - Condition 1: Rows where 'Temperature' matches 'Lower' than '30' + - Condition 2: Rows where 'Altitude' matches 'Greater' than '100' + + When you have more than one condition, you can choose if you want the action (include / exclude) to be applied on rows that **Match all** conditions or **Match any** of the conditions you added. + + In the example above, we chose **Match all** because we wanted to include the rows that have a temperature lower than 30°C *AND* an altitude higher than 100. If we wanted to include the rows that have a temperature lower than 30°C *OR* an altitude higher than 100 instead, then we would select **Match any**. This would include the first row in the original data, which has a temperature of 32°C (does not match the first condition) but an altitude of 101 (which matches the second condition), so it is included. + + Conditions that are invalid or incompletely configured are ignored. + `; + }, + }, + filterFieldsByName: { + name: 'Filter fields by name', + getHelperDocs: function (imageRenderType: ImageRenderType = ImageRenderType.ShortcodeFigure) { + return ` + Use this transformation to remove parts of the query results. + + You can filter field names in three different ways: + + - [Using a regular expression](#use-a-regular-expression) + - [Manually selecting included fields](#manually-select-included-fields) + - [Using a dashboard variable](#use-a-dashboard-variable) + + #### Use a regular expression + + When you filter using a regular expression, field names that match the regular expression are included. + + From the input data: + + | Time | dev-eu-west | dev-eu-north | prod-eu-west | prod-eu-north | + | ------------------- | ----------- | ------------ | ------------ | ------------- | + | 2023-03-04 23:56:23 | 23.5 | 24.5 | 22.2 | 20.2 | + | 2023-03-04 23:56:23 | 23.6 | 24.4 | 22.1 | 20.1 | + + The result from using the regular expression 'prod.*' would be: + + | Time | prod-eu-west | prod-eu-north | + | ------------------- | ------------ | ------------- | + | 2023-03-04 23:56:23 | 22.2 | 20.2 | + | 2023-03-04 23:56:23 | 22.1 | 20.1 | + + The regular expression can include an interpolated dashboard variable by using the \${$${'variableName'}} syntax. + + #### Manually select included fields + + Click and uncheck the field names to remove them from the result. Fields that are matched by the regular expression are still included, even if they're unchecked. + + #### Use a dashboard variable + + Enable 'From variable' to let you select a dashboard variable that's used to include fields. By setting up a [dashboard variable][] with multiple choices, the same fields can be displayed across multiple visualizations. + + ${buildImageContent( + '/static/img/docs/transformations/filter-name-table-before-7-0.png', + imageRenderType, + this.name + 1 + )} + + Here's the table after we applied the transformation to remove the Min field. + + ${buildImageContent( + '/static/img/docs/transformations/filter-name-table-after-7-0.png', + imageRenderType, + this.name + 2 + )} + + Here is the same query using a Stat visualization. + + ${buildImageContent( + '/static/img/docs/transformations/filter-name-stat-after-7-0.png', + imageRenderType, + this.name + 3 + )} + `; + }, + }, + formatString: { + name: 'Format string', + getHelperDocs: function () { + return ` + > **Note:** This transformation is an experimental feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. Enable the 'formatString' in Grafana to use this feature. Contact Grafana Support to enable this feature in Grafana Cloud. + + Use this transformation to format the output of a string field. You can format output in the following ways: + + - Upper case - Formats the entire string in upper case characters. + - Lower case - Formats the entire string in lower case characters. + - Sentence case - Formats the the first character of the string in upper case. + - Title case - Formats the first character of each word in the string in upper case. + - Pascal case - Formats the first character of each word in the string in upper case and doesn't include spaces between words. + - Camel case - Formats the first character of each word in the string in upper case, except the first word, and doesn't include spaces between words. + - Snake case - Formats all characters in the string in lower case and uses underscores instead of spaces between words. + - Kebab case - Formats all characters in the string in lower case and uses dashes instead of spaces between words. + - Trim - Removes all leading and trailing spaces from the string. + - Substring - Returns a substring of the string, using the specified start and end positions. + `; + }, + }, + formatTime: { + name: 'Format time', + getHelperDocs: function () { + return ` + Use this transformation to format the output of a time field. Output can be formatted using [Moment.js format strings](https://momentjs.com/docs/#/displaying/). For instance, if you would like to display only the year of a time field the format string 'YYYY' can be used to show the calendar year (e.g. 1999, 2012, etc.). + + > **Note:** This transformation is available in Grafana 10.1+ as an alpha feature. + `; + }, + }, + groupBy: { + name: 'Group by', + getHelperDocs: function () { + return ` + Use this transformation to group the data by a specified field (column) value and process calculations on each group. Click to see a list of calculation choices. For information about available calculations, refer to [Calculation types][]. + + Here's an example of original data. + + | Time | Server ID | CPU Temperature | Server Status | + | ------------------- | --------- | --------------- | ------------- | + | 2020-07-07 11:34:20 | server 1 | 80 | Shutdown | + | 2020-07-07 11:34:20 | server 3 | 62 | OK | + | 2020-07-07 10:32:20 | server 2 | 90 | Overload | + | 2020-07-07 10:31:22 | server 3 | 55 | OK | + | 2020-07-07 09:30:57 | server 3 | 62 | Rebooting | + | 2020-07-07 09:30:05 | server 2 | 88 | OK | + | 2020-07-07 09:28:06 | server 1 | 80 | OK | + | 2020-07-07 09:25:05 | server 2 | 88 | OK | + | 2020-07-07 09:23:07 | server 1 | 86 | OK | + + This transformation goes in two steps. First you specify one or multiple fields to group the data by. This will group all the same values of those fields together, as if you sorted them. For instance if we group by the Server ID field, then it would group the data this way: + + | Time | Server ID | CPU Temperature | Server Status | + | ------------------- | -------------- | --------------- | ------------- | + | 2020-07-07 11:34:20 | **server 1** | 80 | Shutdown | + | 2020-07-07 09:28:06 | **server 1** | 80 | OK | + | 2020-07-07 09:23:07 | **server 1** | 86 | OK | + | 2020-07-07 10:32:20 | server 2 | 90 | Overload | + | 2020-07-07 09:30:05 | server 2 | 88 | OK | + | 2020-07-07 09:25:05 | server 2 | 88 | OK | + | 2020-07-07 11:34:20 | **_server 3_** | 62 | OK | + | 2020-07-07 10:31:22 | **_server 3_** | 55 | OK | + | 2020-07-07 09:30:57 | **_server 3_** | 62 | Rebooting | + + All rows with the same value of Server ID are grouped together. + + After choosing which field you want to group your data by, you can add various calculations on the other fields, and apply the calculation to each group of rows. For instance, we could want to calculate the average CPU temperature for each of those servers. So we can add the _mean_ calculation applied on the CPU Temperature field to get the following: + + | Server ID | CPU Temperature (mean) | + | --------- | ---------------------- | + | server 1 | 82 | + | server 2 | 88.6 | + | server 3 | 59.6 | + + And we can add more than one calculation. For instance: + + - For field Time, we can calculate the _Last_ value, to know when the last data point was received for each server + - For field Server Status, we can calculate the _Last_ value to know what is the last state value for each server + - For field Temperature, we can also calculate the _Last_ value to know what is the latest monitored temperature for each server + + We would then get : + + | Server ID | CPU Temperature (mean) | CPU Temperature (last) | Time (last) | Server Status (last) | + | --------- | ---------------------- | ---------------------- | ------------------- | -------------------- | + | server 1 | 82 | 80 | 2020-07-07 11:34:20 | Shutdown | + | server 2 | 88.6 | 90 | 2020-07-07 10:32:20 | Overload | + | server 3 | 59.6 | 62 | 2020-07-07 11:34:20 | OK | + + This transformation enables you to extract key information from your time series and display it in a convenient way. + `; + }, + links: [ + { + title: 'Calculation types', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/calculation-types/', + }, + ], + }, + groupingToMatrix: { + name: 'Grouping to matrix', + getHelperDocs: function () { + return ` + Use this transformation to combine three fields-that will be used as input for the **Column**, **Row**, and **Cell value** fields-from the query output, and generate a matrix. This matrix will be calculated as follows: + + **Original data** + + | Server ID | CPU Temperature | Server Status | + | --------- | --------------- | ------------- | + | server 1 | 82 | OK | + | server 2 | 88.6 | OK | + | server 3 | 59.6 | Shutdown | + + We can generate a matrix using the values of 'Server Status' as column names, the 'Server ID' values as row names, and the 'CPU Temperature' as content of each cell. The content of each cell will appear for the existing column ('Server Status') and row combination ('Server ID'). For the rest of the cells, you can select which value to display between: **Null**, **True**, **False**, or **Empty**. + + **Output** + + | Server ID\Server Status | OK | Shutdown | + | ----------------------- | ---- | -------- | + | server 1 | 82 | | + | server 2 | 88.6 | | + | server 3 | | 59.6 | + `; + }, + }, + heatmap: { + name: 'Create heatmap', + getHelperDocs: function () { + return ` + Use this transformation to prepare histogram data to be visualized over time. Similar to the Heatmap panel, this transformation allows you to convert histogram metrics to buckets over time. + + #### X Bucket + + This setting determines how the x-axis is split into buckets. + + - **Size** - Specify a time interval in the input field. For example, a time range of '1h' makes the cells one hour wide on the x-axis. + - **Count** - For non-time related series, use this option to define the number of elements in a bucket. + + #### Y Bucket + + This setting determines how the y-axis is split into buckets. + + - **Linear** + - **Logarithmic** - Use a base 2 or base 10. + - **Symlog** - A symmetrical logarithmic scale. Use a base 2 or base 10; allows negative values. + `; + }, + }, + histogram: { + name: 'Histogram', + getHelperDocs: function () { + return ` + Use this transformation to generate a histogram based on the input data. + + - **Bucket size** - The distance between the lowest item in the bucket (xMin) and the highest item in the bucket (xMax). + - **Bucket offset** - The offset for non-zero based buckets. + - **Combine series** - Create a histogram using all the available series. + + **Original data** + + Series 1: + + | A | B | C | + | --- | --- | --- | + | 1 | 3 | 5 | + | 2 | 4 | 6 | + | 3 | 5 | 7 | + | 4 | 6 | 8 | + | 5 | 7 | 9 | + + Series 2: + + | C | + | --- | + | 5 | + | 6 | + | 7 | + | 8 | + | 9 | + + **Output** + + | xMin | xMax | A | B | C | C | + | ---- | ---- | --- | --- | --- | --- | + | 1 | 2 | 1 | 0 | 0 | 0 | + | 2 | 3 | 1 | 0 | 0 | 0 | + | 3 | 4 | 1 | 1 | 0 | 0 | + | 4 | 5 | 1 | 1 | 0 | 0 | + | 5 | 6 | 1 | 1 | 1 | 1 | + | 6 | 7 | 0 | 1 | 1 | 1 | + | 7 | 8 | 0 | 1 | 1 | 1 | + | 8 | 9 | 0 | 0 | 1 | 1 | + | 9 | 10 | 0 | 0 | 1 | 1 | + `; + }, + }, + joinByField: { + name: 'Join by field', + getHelperDocs: function (imageRenderType: ImageRenderType = ImageRenderType.ShortcodeFigure) { + return ` + Use this transformation to join multiple results into a single table. This is especially useful for converting multiple + time series results into a single wide table with a shared time field. + + #### Inner join + + An inner join merges data from multiple tables where all tables share the same value from the selected field. This type of join excludes + data where values do not match in every result. + + Use this transformation to combine the results from multiple queries (combining on a passed join field or the first time column) into one result, and drop rows where a successful join cannot occur. + + In the following example, two queries return table data. It is visualized as two separate tables before applying the inner join transformation. + + Query A: + + | Time | Job | Uptime | + | ------------------- | ------- | --------- | + | 2020-07-07 11:34:20 | node | 25260122 | + | 2020-07-07 11:24:20 | postgre | 123001233 | + | 2020-07-07 11:14:20 | postgre | 345001233 | + + Query B: + + | Time | Server | Errors | + | ------------------- | -------- | ------ | + | 2020-07-07 11:34:20 | server 1 | 15 | + | 2020-07-07 11:24:20 | server 2 | 5 | + | 2020-07-07 11:04:20 | server 3 | 10 | + + The result after applying the inner join transformation looks like the following: + + | Time | Job | Uptime | Server | Errors | + | ------------------- | ------- | --------- | -------- | ------ | + | 2020-07-07 11:34:20 | node | 25260122 | server 1 | 15 | + | 2020-07-07 11:24:20 | postgre | 123001233 | server 2 | 5 | + + #### Outer join + + An outer join includes all data from an inner join and rows where values do not match in every input. While the inner join joins Query A and Query B on the time field, the outer join includes all rows that don't match on the time field. + + In the following example, two queries return table data. It is visualized as two tables before applying the outer join transformation. + + Query A: + + | Time | Job | Uptime | + | ------------------- | ------- | --------- | + | 2020-07-07 11:34:20 | node | 25260122 | + | 2020-07-07 11:24:20 | postgre | 123001233 | + | 2020-07-07 11:14:20 | postgre | 345001233 | + + Query B: + + | Time | Server | Errors | + | ------------------- | -------- | ------ | + | 2020-07-07 11:34:20 | server 1 | 15 | + | 2020-07-07 11:24:20 | server 2 | 5 | + | 2020-07-07 11:04:20 | server 3 | 10 | + + The result after applying the outer join transformation looks like the following: + + | Time | Job | Uptime | Server | Errors | + | ------------------- | ------- | --------- | -------- | ------ | + | 2020-07-07 11:04:20 | | | server 3 | 10 | + | 2020-07-07 11:14:20 | postgre | 345001233 | | | + | 2020-07-07 11:34:20 | node | 25260122 | server 1 | 15 | + | 2020-07-07 11:24:20 | postgre | 123001233 | server 2 | 5 | + + In the following example, a template query displays time series data from multiple servers in a table visualization. The results of only one query can be viewed at a time. + + ${buildImageContent('/static/img/docs/transformations/join-fields-before-7-0.png', imageRenderType, this.name + 1)} + + I applied a transformation to join the query results using the time field. Now I can run calculations, combine, and organize the results in this new table. + + ${buildImageContent('/static/img/docs/transformations/join-fields-after-7-0.png', imageRenderType, this.name + 2)} + `; + }, + }, + joinByLabels: { + name: 'Join by labels', + getHelperDocs: function () { + return ` + Use this transformation to join multiple results into a single table. This is especially useful for converting multiple + time series results into a single wide table with a shared **Label** field. + + - **Join** - Select the label to join by between the labels available or common across all time series. + - **Value** - The name for the output result. + + #### Example + + ##### Input + + series1{what="Temp", cluster="A", job="J1"} + + | Time | Value | + | ---- | ----- | + | 1 | 10 | + | 2 | 200 | + + series2{what="Temp", cluster="B", job="J1"} + + | Time | Value | + | ---- | ----- | + | 1 | 10 | + | 2 | 200 | + + series3{what="Speed", cluster="B", job="J1"} + + | Time | Value | + | ---- | ----- | + | 22 | 22 | + | 28 | 77 | + + ##### Config + + value: "what" + + ##### Output + + | cluster | job | Temp | Speed | + | ------- | --- | ---- | ----- | + | A | J1 | 10 | | + | A | J1 | 200 | | + | B | J1 | 10 | 22 | + | B | J1 | 200 | 77 | + `; + }, + }, + labelsToFields: { + name: 'Labels to fields', + getHelperDocs: function () { + return ` + Use this transformation to change time series results that include labels or tags into a table where each label's keys and values are included in the table result. The labels can be displayed as either columns or row values. + + Given a query result of two time series: + + - Series 1: labels Server=Server A, Datacenter=EU + - Series 2: labels Server=Server B, Datacenter=EU + + In "Columns" mode, the result looks like this: + + | Time | Server | Datacenter | Value | + | ------------------- | -------- | ---------- | ----- | + | 2020-07-07 11:34:20 | Server A | EU | 1 | + | 2020-07-07 11:34:20 | Server B | EU | 2 | + + In "Rows" mode, the result has a table for each series and show each label value like this: + + | label | value | + | ---------- | -------- | + | Server | Server A | + | Datacenter | EU | + + | label | value | + | ---------- | -------- | + | Server | Server B | + | Datacenter | EU | + + #### Value field name + + If you selected Server as the **Value field name**, then you would get one field for every value of the Server label. + + | Time | Datacenter | Server A | Server B | + | ------------------- | ---------- | -------- | -------- | + | 2020-07-07 11:34:20 | EU | 1 | 2 | + + #### Merging behavior + + The labels to fields transformer is internally two separate transformations. The first acts on single series and extracts labels to fields. The second is the [merge](#merge) transformation that joins all the results into a single table. The merge transformation tries to join on all matching fields. This merge step is required and cannot be turned off. + + To illustrate this, here is an example where you have two queries that return time series with no overlapping labels. + + - Series 1: labels Server=ServerA + - Series 2: labels Datacenter=EU + + This will first result in these two tables: + + | Time | Server | Value | + | ------------------- | ------- | ----- | + | 2020-07-07 11:34:20 | ServerA | 10 | + + | Time | Datacenter | Value | + | ------------------- | ---------- | ----- | + | 2020-07-07 11:34:20 | EU | 20 | + + After merge: + + | Time | Server | Value | Datacenter | + | ------------------- | ------- | ----- | ---------- | + | 2020-07-07 11:34:20 | ServerA | 10 | | + | 2020-07-07 11:34:20 | | 20 | EU | + `; + }, + }, + limit: { + name: 'Limit', + getHelperDocs: function () { + return ` + Use this transformation to limit the number of rows displayed. + + In the example below, we have the following response from the data source: + + | Time | Metric | Value | + | ------------------- | ----------- | ----- | + | 2020-07-07 11:34:20 | Temperature | 25 | + | 2020-07-07 11:34:20 | Humidity | 22 | + | 2020-07-07 10:32:20 | Humidity | 29 | + | 2020-07-07 10:31:22 | Temperature | 22 | + | 2020-07-07 09:30:57 | Humidity | 33 | + | 2020-07-07 09:30:05 | Temperature | 19 | + + Here is the result after adding a Limit transformation with a value of '3': + + | Time | Metric | Value | + | ------------------- | ----------- | ----- | + | 2020-07-07 11:34:20 | Temperature | 25 | + | 2020-07-07 11:34:20 | Humidity | 22 | + | 2020-07-07 10:32:20 | Humidity | 29 | + `; + }, + }, + merge: { + name: 'Merge', + getHelperDocs: function () { + return ` + Use this transformation to combine the result from multiple queries into one single result. This is helpful when using the table panel visualization. Values that can be merged are combined into the same row. Values are mergeable if the shared fields contain the same data. For information, refer to [Table panel][]. + + In the example below, we have two queries returning table data. It is visualized as two separate tables before applying the transformation. + + Query A: + + | Time | Job | Uptime | + | ------------------- | ------- | --------- | + | 2020-07-07 11:34:20 | node | 25260122 | + | 2020-07-07 11:24:20 | postgre | 123001233 | + + Query B: + + | Time | Job | Errors | + | ------------------- | ------- | ------ | + | 2020-07-07 11:34:20 | node | 15 | + | 2020-07-07 11:24:20 | postgre | 5 | + + Here is the result after applying the Merge transformation. + + | Time | Job | Errors | Uptime | + | ------------------- | ------- | ------ | --------- | + | 2020-07-07 11:34:20 | node | 15 | 25260122 | + | 2020-07-07 11:24:20 | postgre | 5 | 123001233 | + `; + }, + links: [ + { + title: 'Table panel', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/table/', + }, + ], + }, + organize: { + name: 'Oraganize fields', + getHelperDocs: function () { + return ` + Use this transformation to rename, reorder, or hide fields returned by the query. + + > **Note:** This transformation only works in panels with a single query. If your panel has multiple queries, then you must either apply an Outer join transformation or remove the extra queries. + + Grafana displays a list of fields returned by the query. You can: + + - Change field order by hovering your cursor over a field. The cursor turns into a hand and then you can drag the field to its new place. + - Hide or show a field by clicking the eye icon next to the field name. + - Rename fields by typing a new name in the **Rename ** box. + `; + }, + }, + partitionByValues: { + name: 'Partition by values', + getHelperDocs: function () { + return ` + Use this transformation to eliminate the need for multiple queries to the same data source with different 'WHERE' clauses when graphing multiple series. Consider a metrics SQL table with the following data: + + | Time | Region | Value | + | ------------------- | ------ | ----- | + | 2022-10-20 12:00:00 | US | 1520 | + | 2022-10-20 12:00:00 | EU | 2936 | + | 2022-10-20 01:00:00 | US | 1327 | + | 2022-10-20 01:00:00 | EU | 912 | + + Prior to v9.3, if you wanted to plot a red trendline for US and a blue one for EU in the same TimeSeries panel, you would likely have to split this into two queries: + + 'SELECT Time, Value FROM metrics WHERE Time > "2022-10-20" AND Region="US"'
+ 'SELECT Time, Value FROM metrics WHERE Time > "2022-10-20" AND Region="EU"' + + This also requires you to know ahead of time which regions actually exist in the metrics table. + + With the _Partition by values_ transformer, you can now issue a single query and split the results by unique values in one or more columns ('fields') of your choosing. The following example uses 'Region'. + + 'SELECT Time, Region, Value FROM metrics WHERE Time > "2022-10-20"' + + | Time | Region | Value | + | ------------------- | ------ | ----- | + | 2022-10-20 12:00:00 | US | 1520 | + | 2022-10-20 01:00:00 | US | 1327 | + + | Time | Region | Value | + | ------------------- | ------ | ----- | + | 2022-10-20 12:00:00 | EU | 2936 | + | 2022-10-20 01:00:00 | EU | 912 | + `; + }, + }, + prepareTimeSeries: { + name: 'Prepare time series', + getHelperDocs: function () { + return ` + Use this transformation when a data source returns time series data in a format that isn't supported by the panel you want to use. For more information about data frame formats, refer to [Data frames][]. + + This transformation helps you resolve this issue by converting the time series data from either the wide format to the long format or the other way around. + + Select the 'Multi-frame time series' option to transform the time series data frame from the wide to the long format. + + Select the 'Wide time series' option to transform the time series data frame from the long to the wide format. + + > **Note:** This transformation is available in Grafana 7.5.10+ and Grafana 8.0.6+. + `; + }, + links: [ + { + title: 'Data frames', + url: 'https://grafana.com/docs/grafana/latest/developers/plugins/introduction-to-plugin-development/data-frames/', + }, + ], + }, + reduce: { + name: 'Reduce', + getHelperDocs: function () { + return ` + Use this transformation to apply a calculation to each field in the frame and return a single value. Time fields are removed when applying this transformation. + + Consider the input: + + Query A: + + | Time | Temp | Uptime | + | ------------------- | ---- | ------- | + | 2020-07-07 11:34:20 | 12.3 | 256122 | + | 2020-07-07 11:24:20 | 15.4 | 1230233 | + + Query B: + + | Time | AQI | Errors | + | ------------------- | --- | ------ | + | 2020-07-07 11:34:20 | 6.5 | 15 | + | 2020-07-07 11:24:20 | 3.2 | 5 | + + The reduce transformer has two modes: + + - **Series to rows -** Creates a row for each field and a column for each calculation. + - **Reduce fields -** Keeps the existing frame structure, but collapses each field into a single value. + + For example, if you used the **First** and **Last** calculation with a **Series to rows** transformation, then + the result would be: + + | Field | First | Last | + | ------ | ------ | ------- | + | Temp | 12.3 | 15.4 | + | Uptime | 256122 | 1230233 | + | AQI | 6.5 | 3.2 | + | Errors | 15 | 5 | + + The **Reduce fields** with the **Last** calculation, + results in two frames, each with one row: + + Query A: + + | Temp | Uptime | + | ---- | ------- | + | 15.4 | 1230233 | + + Query B: + + | AQI | Errors | + | --- | ------ | + | 3.2 | 5 | + `; + }, + }, + renameByRegex: { + name: 'Rename by regex', + getHelperDocs: function (imageRenderType: ImageRenderType = ImageRenderType.ShortcodeFigure) { + return ` + Use this transformation to rename parts of the query results using a regular expression and replacement pattern. + + You can specify a regular expression, which is only applied to matches, along with a replacement pattern that support back references. For example, let's imagine you're visualizing CPU usage per host and you want to remove the domain name. You could set the regex to '([^\.]+)\..+' and the replacement pattern to '$1', 'web-01.example.com' would become 'web-01'. + + In the following example, we are stripping the prefix from event types. In the before image, you can see everything is prefixed with 'system.' + + ${buildImageContent( + '/static/img/docs/transformations/rename-by-regex-before-7-3.png', + imageRenderType, + this.name + 1 + )} + + With the transformation applied, you can see we are left with just the remainder of the string. + + ${buildImageContent('/static/img/docs/transformations/rename-by-regex-after-7-3.png', imageRenderType, this.name + 1)} + `; + }, + }, + rowsToFields: { + name: 'Rows to fields', + getHelperDocs: function () { + return ` + Use this transformation to convert rows into separate fields. This can be useful because fields can be styled and configured individually. It can also use additional fields as sources for dynamic field configuration or map them to field labels. The additional labels can then be used to define better display names for the resulting fields. + + This transformation includes a field table which lists all fields in the data returned by the config query. This table gives you control over what field should be mapped to each config property (the \*Use as\*\* option). You can also choose which value to select if there are multiple rows in the returned data. + + This transformation requires: + + - One field to use as the source of field names. + + By default, the transform uses the first string field as the source. You can override this default setting by selecting **Field name** in the **Use as** column for the field you want to use instead. + + - One field to use as the source of values. + + By default, the transform uses the first number field as the source. But you can override this default setting by selecting **Field value** in the **Use as** column for the field you want to use instead. + + Useful when visualizing data in: + + - Gauge + - Stat + - Pie chart + + #### Map extra fields to labels + + If a field does not map to config property Grafana will automatically use it as source for a label on the output field- + + Example: + + | Name | DataCenter | Value | + | ------- | ---------- | ----- | + | ServerA | US | 100 | + | ServerB | EU | 200 | + + Output: + + | ServerA (labels: DataCenter: US) | ServerB (labels: DataCenter: EU) | + | -------------------------------- | -------------------------------- | + | 10 | 20 | + + The extra labels can now be used in the field display name provide more complete field names. + + If you want to extract config from one query and apply it to another you should use the config from query results transformation. + + #### Example + + Input: + + | Name | Value | Max | + | ------- | ----- | --- | + | ServerA | 10 | 100 | + | ServerB | 20 | 200 | + | ServerC | 30 | 300 | + + Output: + + | ServerA (config: max=100) | ServerB (config: max=200) | ServerC (config: max=300) | + | ------------------------- | ------------------------- | ------------------------- | + | 10 | 20 | 30 | + + As you can see each row in the source data becomes a separate field. Each field now also has a max config option set. Options like **Min**, **Max**, **Unit** and **Thresholds** are all part of field configuration and if set like this will be used by the visualization instead of any options manually configured in the panel editor options pane. + `; + }, + }, + seriesToRows: { + name: 'Series to rows', + getHelperDocs: function () { + return ` + Use this transformation to combine the result from multiple time series data queries into one single result. This is helpful when using the table panel visualization. + + The result from this transformation will contain three columns: Time, Metric, and Value. The Metric column is added so you easily can see from which query the metric originates from. Customize this value by defining Label on the source query. + + In the example below, we have two queries returning time series data. It is visualized as two separate tables before applying the transformation. + + Query A: + + | Time | Temperature | + | ------------------- | ----------- | + | 2020-07-07 11:34:20 | 25 | + | 2020-07-07 10:31:22 | 22 | + | 2020-07-07 09:30:05 | 19 | + + Query B: + + | Time | Humidity | + | ------------------- | -------- | + | 2020-07-07 11:34:20 | 24 | + | 2020-07-07 10:32:20 | 29 | + | 2020-07-07 09:30:57 | 33 | + + Here is the result after applying the Series to rows transformation. + + | Time | Metric | Value | + | ------------------- | ----------- | ----- | + | 2020-07-07 11:34:20 | Temperature | 25 | + | 2020-07-07 11:34:20 | Humidity | 22 | + | 2020-07-07 10:32:20 | Humidity | 29 | + | 2020-07-07 10:31:22 | Temperature | 22 | + | 2020-07-07 09:30:57 | Humidity | 33 | + | 2020-07-07 09:30:05 | Temperature | 19 | + + > **Note:** This transformation is available in Grafana 7.1+. + `; + }, + }, + sortBy: { + name: 'Sort by', + getHelperDocs: function () { + return ` + Use this transformation to sort each frame by the configured field. When the **Reverse** switch is on, the values will return in the opposite order. + `; + }, + }, + spatial: { + name: 'Spatial', + getHelperDocs: function () { + // This template string space-formatting is intentional. + return ` + Use this transformation to apply spatial operations to query results + `; + }, + }, + timeSeriesTable: { + name: 'Time series to table transform', + getHelperDocs: function () { + return ` + Use this transformation to convert time series result into a table, converting time series data frame into a "Trend" field. "Trend" field can then be rendered using [sparkline cell type][], producing an inline sparkline for each table row. If there are multiple time series queries, each will result in a separate table data frame. These can be joined using join or merge transforms to produce a single table with multiple sparklines per row. + + For each generated "Trend" field value calculation function can be selected. Default is "last non null value". This value will be displayed next to the sparkline and used for sorting table rows. + + > **Note:** This transformation is available in Grafana 9.5+ as an opt-in beta feature. Modify Grafana [configuration file][] to use it. + `; + }, + links: [ + { + title: 'sparkline cell type', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/table/#sparkline', + }, + { + title: 'configuration file', + url: 'https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/', + }, + ], + }, +}; + +export function getLinkToDocs(): string { + return ` + Go to the + transformation documentation + for more general documentation. + `; +} + +function buildImageContent(source: string, imageRenderType: ImageRenderType, imageName?: string) { + return imageRenderType === 'shortcodeFigure' + ? // This will build a Hugo Shortcode "figure" image template, which shares the same default class and max-width. + `{{< figure src="${source}" class="docs-image--no-shadow" max-width= "1100px" >}}` + : // This will build generic Markdown image syntax for UI rendering. + `![${imageName} helper image](${source})`; +} diff --git a/scripts/docs/generate-transformations.ts b/scripts/docs/generate-transformations.ts new file mode 100644 index 00000000000..9c1064618b5 --- /dev/null +++ b/scripts/docs/generate-transformations.ts @@ -0,0 +1,177 @@ +import { + transformationDocsContent, + TransformationDocsContentType, + ImageRenderType, +} from '../../public/app/features/transformers/docs/content'; + +const template = `--- +comments: | + This Markdown file is auto-generated. Do not edit this file directly. + To build this Markdown, do the following: + + $ cd /docs (from the root of the repository) + $ make sources/panels-visualizations/query-transform-data/transform-data/index.md + $ make docs + + Browse to http://localhost:3003/docs/grafana/latest/panels-visualizations/query-transform-data/transform-data/ + + Refer to ./docs/README.md "Content guidelines" for more information about editing and building these docs. + +aliases: + - ../../panels/reference-transformation-functions/ + - ../../panels/transform-data/ + - ../../panels/transform-data/about-transformation/ + - ../../panels/transform-data/add-transformation-to-data/ + - ../../panels/transform-data/apply-transformation-to-data/ + - ../../panels/transform-data/debug-transformation/ + - ../../panels/transform-data/delete-transformation/ + - ../../panels/transform-data/transformation-functions/ + - ../../panels/transformations/ + - ../../panels/transformations/apply-transformations/ + - ../../panels/transformations/config-from-query/ + - ../../panels/transformations/rows-to-fields/ + - ../../panels/transformations/types-options/ +labels: + products: + - cloud + - enterprise + - oss +title: Transform data +weight: 100 +--- + +# Transform data + +Transformations are a powerful way to manipulate data returned by a query before the system applies a visualization. Using transformations, you can: + +- Rename fields +- Join time series data +- Perform mathematical operations across queries +- Use the output of one transformation as the input to another transformation + +For users that rely on multiple views of the same dataset, transformations offer an efficient method of creating and maintaining numerous dashboards. + +You can also use the output of one transformation as the input to another transformation, which results in a performance gain. + +> Sometimes the system cannot graph transformed data. When that happens, click the \`Table view\` toggle above the visualization to switch to a table view of the data. This can help you understand the final result of your transformations. + +## Transformation types + +Grafana provides a number of ways that you can transform data. For a complete list of transformations, refer to [Transformation functions](#transformation-functions). + +## Order of transformations + +When there are multiple transformations, Grafana applies them in the order they are listed. Each transformation creates a result set that then passes on to the next transformation in the processing pipeline. + +The order in which Grafana applies transformations directly impacts the results. For example, if you use a Reduce transformation to condense all the results of one column into a single value, then you can only apply transformations to that single value. + +## Add a transformation function to data + +The following steps guide you in adding a transformation to data. This documentation does not include steps for each type of transformation. For a complete list of transformations, refer to [Transformation functions](#transformation-functions). + +1. Navigate to the panel where you want to add one or more transformations. +1. Hover over any part of the panel to display the actions menu on the top right corner. +1. Click the menu and select **Edit**. +1. Click the **Transform** tab. +1. Click a transformation. + A transformation row appears where you configure the transformation options. For more information about how to configure a transformation, refer to [Transformation functions](#transformation-functions). + For information about available calculations, refer to [Calculation types][]. +1. To apply another transformation, click **Add transformation**. + This transformation acts on the result set returned by the previous transformation. + {{< figure src="/static/img/docs/transformations/transformations-7-0.png" class="docs-image--no-shadow" max-width= "1100px" >}} + +## Debug a transformation + +To see the input and the output result sets of the transformation, click the bug icon on the right side of the transformation row. + +The input and output results sets can help you debug a transformation. + +{{< figure src="/static/img/docs/transformations/debug-transformations-7-0.png" class="docs-image--no-shadow" max-width= "1100px" >}} + +## Disable a transformation + +You can disable or hide one or more transformations by clicking on the eye icon on the top right side of the transformation row. This disables the applied actions of that specific transformation and can help to identify issues when you change several transformations one after another. + +{{< figure src="/static/img/docs/transformations/screenshot-example-disable-transformation.png" class="docs-image--no-shadow" max-width= "1100px" >}} + +## Filter a transformation + +If your panel uses more than one query, you can filter these and apply the selected transformation to only one of the queries. To do this, click the filter icon on the top right of the transformation row. This opens a drop-down with a list of queries used on the panel. From here, you can select the query you want to transform. + +Note that the filter icon is always displayed if your panel has more than one query, but it may not work if previous transformations for merging the queries' outputs are applied. This is because one transformation takes the output of the previous one. + +## Delete a transformation + +We recommend that you remove transformations that you don't need. When you delete a transformation, you remove the data from the visualization. + +**Before you begin:** + +- Identify all dashboards that rely on the transformation and inform impacted dashboard users. + +**To delete a transformation**: + +1. Open a panel for editing. +1. Click the **Transform** tab. +1. Click the trash icon next to the transformation you want to delete. + +{{< figure src="/static/img/docs/transformations/screenshot-example-remove-transformation.png" class="docs-image--no-shadow" max-width= "1100px" >}} + +## Transformation functions + +You can perform the following transformations on your data. + +${buildTransformationDocsContent(transformationDocsContent)} + +{{% docs/reference %}} +[Table panel]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/table" +[Table panel]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/visualizations/table" + +[Data frames]: "/docs/grafana/ -> /docs/grafana//developers/plugins/introduction-to-plugin-development/data-frames" +[Data frames]: "/docs/grafana-cloud/ -> /docs/grafana//developers/plugins/introduction-to-plugin-development/data-frames" + +[Calculation types]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/calculation-types" +[Calculation types]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/calculation-types" + +[sparkline cell type]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/table#sparkline" +[sparkline cell type]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/visualizations/table#sparkline" + +[Heatmap panel]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/heatmap" +[Heatmap panel]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/visualizations/heatmap" + +[configuration file]: "/docs/grafana/ -> /docs/grafana//setup-grafana/configure-grafana#configuration-file-location" +[configuration file]: "/docs/grafana-cloud/ -> /docs/grafana//setup-grafana/configure-grafana#configuration-file-location" + +[Time series panel]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/time-series" +[Time series panel]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/visualizations/time-series" + +[feature toggle]: "/docs/grafana/ -> /docs/grafana//setup-grafana/configure-grafana#feature_toggles" +[feature toggle]: "/docs/grafana-cloud/ -> /docs/grafana//setup-grafana/configure-grafana#feature_toggles" + +[dashboard variable]: "/docs/grafana/ -> docs/grafana//dashboards/variables" +[dashboard variable]: "/docs/grafana-cloud/ -> docs/grafana//dashboards/variables" + +{{% /docs/reference %}} +`; + +function buildTransformationDocsContent(transformationDocsContent: TransformationDocsContentType) { + const transformationsList = Object.keys(transformationDocsContent); + + const content = transformationsList + .map((transformationName) => { + return ` + ### ${transformationDocsContent[transformationName].name} + ${transformationDocsContent[transformationName].getHelperDocs(ImageRenderType.ShortcodeFigure)} + `; + }) + // Remove the superfluous commas. + .join(''); + + return content; +} + +/* + `process.stdout.write(template + '\n')` was not functioning as expected. + Neither the tsc nor ts-node compiler could identify the node `process` object. + Fortunately, `console.log` also writes to the standard output. +*/ +console.log(template); From 9a563a4d19bb70c5735dc257aa4f2a9ec2c58b65 Mon Sep 17 00:00:00 2001 From: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> Date: Tue, 24 Oct 2023 09:30:45 -0700 Subject: [PATCH 038/180] Docs: Clarify open source documentation (#77077) * Docs: Clarify open source documentation * Update docs/sources/_index.md Co-authored-by: Eve Meelan <81647476+Eve832@users.noreply.github.com> --------- Co-authored-by: Eve Meelan <81647476+Eve832@users.noreply.github.com> --- docs/sources/_index.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/sources/_index.md b/docs/sources/_index.md index 0fe79be372a..3cf37151df1 100644 --- a/docs/sources/_index.md +++ b/docs/sources/_index.md @@ -3,19 +3,20 @@ aliases: - /docs/grafana/v1.1/ - /docs/grafana/v3.1/ - guides/reference/admin/ -description: Guides, installation, and feature documentation +description: Open source documentation for Grafana keywords: - grafana + - open source - installation - documentation labels: products: - enterprise - oss -title: Grafana documentation +title: Grafana open source documentation --- -# Grafana documentation +# Grafana open source documentation ## Installing Grafana From be6b407d739fd938f1720bd3673d25a0b912799c Mon Sep 17 00:00:00 2001 From: Isabel <76437239+imatwawana@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:56:09 -0400 Subject: [PATCH 039/180] =?UTF-8?q?docs:=20What=E2=80=99s=20new=20&=20Upgr?= =?UTF-8?q?ade=20guide=2010.2=20(#75909)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added 10.2 what's new and upgrade guide * Fixed title and added guidance * add: no basic role feature to whats new * Revert "add: no basic role feature to whats new" This reverts commit eb2e31c3dcf013c45b4102f3a149826f5d4c7fe5. * Add field min/max calculation to what's new * refresh token handling * update * Remove whitespace * Revert "Add field min/max calculation to what's new" This reverts commit 9b66ddd0ecfed3b9c3b1dee9cdac6d748e24cffa. * Revert refresh token handling and move it to separate PR * Docs: Add permission validation enabled by default to what's new (#76376) * Add permission validation enabled by default to whats new * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * update * Apply suggestions from code review Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Fix Grafana editions note * run prettier --------- Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Fixed typo * Docs: Add No basic role feature description (#76345) * add whats next for cloud * Update docs/sources/whatsnew/whats-new-next/index.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-next/index.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-next/index.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-next/index.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-next/index.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-next/index.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> --------- Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Auth: Whats new v10.2 (#76369) * add whatsnew auth * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v10-2.md * Apply suggestions from code review Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * separate ds and db managed permissions whats new * Apply suggestions from code review Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> --------- Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * What's new in transformations 10.2 (#76430) * first draft * spelling * Add doc links and images * Fix relrefs * Apply suggestions from code review Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Add information about availability * update availability for string format transform * add field from calc modifications * Fixed link version syntax * Apply suggestions from code review Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> --------- Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> Co-authored-by: Victor Marin * Add field min/max calculation to what's new (#76401) * Add field min/max calculation to what's new * fix whitespace * Fix which editions it's available in * Apply suggestions from code review Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * change availability * reduce image size --------- Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Removed What's new next addition from this PR * Added alertmanager update (#76584) * Added alertmanager update * Fixed order of items * Added availability info * remove typo Co-authored-by: Oscar Kilhed * Remove heading formatting * initial draft of dataviz squad deliverables for 10.2 whats new * add dashgpt (#dashboard-ai) whats new draft * What's New: Correlations in 10.2 (#76505) * What's new about correlations * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update availability * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> --------- Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Updated linking guidance in template and fixed links * Fixed link Co-authored-by: Kate Brenner <32871890+katebrenner@users.noreply.github.com> * Docs: update whats new links (#76639) Replaced relrefs with fully qualified URLs * DashGPT: update availability status to public preview * Update What's New to Include information on Timezone and Applicability changes in transformations (#76571) * Update what's new * Fix misspelling * Update to fix wording Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Remove merge conflict flag Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Fix capitalization Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Fix wording Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Sentence casing Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Fix up wording Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Fix availability * One last fix :) Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Fixed typo (I introduced!) --------- Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Docs: add Cloud what's new items to 10.2 what's new (#76642) * Added Cloud what's new content * Apply suggestions from code review Co-authored-by: Jennifer Villa * Updated link formatting * Updated Browse Dashboards with the latest content * Updated availability * Updated wording Co-authored-by: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> * Fixed typo Co-authored-by: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> --------- Co-authored-by: Jennifer Villa Co-authored-by: joshhunt Co-authored-by: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> * Fixed title * Removed all editions language * General edits * Copy edits * Fixed availability note * Copy editing Removed Visualize enum entry Added sharing section Added alerting entry Added introduction Made minor copy edits * Fixed capitalization * Fixed typo * add CTA and gif to AI features * update recorded queries text * Update edition availability Cloud permissions were missing (even though this is an OSS release I think it's important to include details about Cloud availability) and some features listed the wrong availability. * reorder topics for emphasis Emphasize the most exciting improvements * add links to feature toggle docs * move recorded queries * update wording and titles for readability * add visualization media * update wording in dataviz section * dashgpt: finalize whats new for 10.2 (finally) 😬 * Fixed typo * DataViz: finalize whats new content 😬 * Copy edits * update enterprise ds availability * Update whats-new-in-v10-2.md * Fixed style issues and availability note * Added missing Alerting items * Updated availability note * Fixed styling * Reverted what's new link to relref * update availability of No Basic Role I consulted with Eric before making this change. * Removed item not going out in this release * Update Alerting items * Fixed linting --------- Co-authored-by: Eric Leijonmarck Co-authored-by: Oscar Kilhed Co-authored-by: Mihaly Gyongyosi Co-authored-by: Jo Co-authored-by: Victor Marin Co-authored-by: nmarrs Co-authored-by: Piotr Jamróz Co-authored-by: Kate Brenner <32871890+katebrenner@users.noreply.github.com> Co-authored-by: Kyle Cunningham Co-authored-by: Jennifer Villa Co-authored-by: joshhunt Co-authored-by: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Co-authored-by: Mitch Seaman Co-authored-by: Mitchel Seaman Co-authored-by: Miguel Palau --- docs/sources/_index.md | 4 +- docs/sources/shared/upgrade/intro.md | 4 +- .../shared/upgrade/upgrade-common-tasks.md | 2 +- .../upgrade-guide/upgrade-v10.2/index.md | 21 + docs/sources/whatsnew/_index.md | 1 + docs/sources/whatsnew/whats-new-in-v10-2.md | 476 ++++++++++++++++++ 6 files changed, 503 insertions(+), 5 deletions(-) create mode 100644 docs/sources/upgrade-guide/upgrade-v10.2/index.md create mode 100644 docs/sources/whatsnew/whats-new-in-v10-2.md diff --git a/docs/sources/_index.md b/docs/sources/_index.md index 3cf37151df1..0ea6107fb35 100644 --- a/docs/sources/_index.md +++ b/docs/sources/_index.md @@ -75,8 +75,8 @@ title: Grafana open source documentation

Provisioning

Learn how to automate your Grafana configuration.

- }}" class="nav-cards__item nav-cards__item--guide"> -

What's new in v10.1

+
}}" class="nav-cards__item nav-cards__item--guide"> +

What's new in v10.2

Explore the features and enhancements in the latest release.

diff --git a/docs/sources/shared/upgrade/intro.md b/docs/sources/shared/upgrade/intro.md index 2f77e8a04ae..108c783c14c 100644 --- a/docs/sources/shared/upgrade/intro.md +++ b/docs/sources/shared/upgrade/intro.md @@ -13,10 +13,10 @@ Because Grafana upgrades are backward compatible, the upgrade process is straigh In addition to common tasks you should complete for all versions of Grafana, there might be additional upgrade tasks to complete for a version. {{% admonition type="note" %}} -There might be breaking changes in some releases. We outline these changes in the [What's New ]({{< relref "../../whatsnew/" >}}) document for most releases or a separate [Breaking changes]({{< relref "../../breaking-changes/" >}}) document for releases with many breaking changes. +There might be breaking changes in some releases. We outline these changes in the [What's New ](https://grafana.com/docs/grafana//whatsnew/) document for most releases or a separate [Breaking changes](https://grafana.com/docs/grafana//breaking-changes/) document for releases with many breaking changes. {{% /admonition %}} -For versions of Grafana prior to v9.2, we published additional information in the [Release Notes]({{< relref "../../release-notes/" >}}). +For versions of Grafana prior to v9.2, we published additional information in the [Release Notes](https://grafana.com/docs/grafana//release-notes/). When available, we list all changes with links to pull requests or issues in the [Changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md). diff --git a/docs/sources/shared/upgrade/upgrade-common-tasks.md b/docs/sources/shared/upgrade/upgrade-common-tasks.md index 921911614ef..3c53e3d44f3 100644 --- a/docs/sources/shared/upgrade/upgrade-common-tasks.md +++ b/docs/sources/shared/upgrade/upgrade-common-tasks.md @@ -67,7 +67,7 @@ To upgrade Grafana installed using RPM or YUM complete the following steps: 1. Perform one of the following steps based on your installation. - - If you [downloaded an RPM package](https://grafana.com/grafana/download) to install Grafana, then complete the steps documented in [Install Grafana on Red Hat, RHEL, or Fedora]({{< relref "../../setup-grafana/installation/redhat-rhel-fedora/" >}}) or [Install Grafana on SUSE or openSUSE]({{< relref "../../setup-grafana/installation/suse-opensuse/" >}}) to upgrade Grafana. + - If you [downloaded an RPM package](https://grafana.com/grafana/download) to install Grafana, then complete the steps documented in [Install Grafana on Red Hat, RHEL, or Fedora](https://grafana.com/docs/grafana//setup-grafana/installation/redhat-rhel-fedora/) or [Install Grafana on SUSE or openSUSE](https://grafana.com/docs/grafana///setup-grafana/installation/suse-opensuse/) to upgrade Grafana. - If you used the Grafana YUM repository, run the following command: ```bash diff --git a/docs/sources/upgrade-guide/upgrade-v10.2/index.md b/docs/sources/upgrade-guide/upgrade-v10.2/index.md new file mode 100644 index 00000000000..17e706d64ef --- /dev/null +++ b/docs/sources/upgrade-guide/upgrade-v10.2/index.md @@ -0,0 +1,21 @@ +--- +description: Guide for upgrading to Grafana v10.2 +keywords: + - grafana + - configuration + - documentation + - upgrade +title: Upgrade to Grafana v10.2 +menuTitle: Upgrade to v10.2 +weight: 1500 +--- + +# Upgrade to Grafana v10.2 + +{{< docs/shared lookup="upgrade/intro.md" source="grafana" version="" >}} + +{{< docs/shared lookup="back-up/back-up-grafana.md" source="grafana" version="" leveloffset="+1" >}} + +{{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="" >}} + +## Technical notes diff --git a/docs/sources/whatsnew/_index.md b/docs/sources/whatsnew/_index.md index 63ab1a5446e..a386419bdf8 100644 --- a/docs/sources/whatsnew/_index.md +++ b/docs/sources/whatsnew/_index.md @@ -76,6 +76,7 @@ For a complete list of every change, with links to pull requests and related iss ## Grafana 10 +- [What's new in 10.2](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v10-2/) - [What's new in 10.1]({{< relref "whats-new-in-v10-1/" >}}) - [What's new in 10.0]({{< relref "whats-new-in-v10-0/" >}}) diff --git a/docs/sources/whatsnew/whats-new-in-v10-2.md b/docs/sources/whatsnew/whats-new-in-v10-2.md new file mode 100644 index 00000000000..1da21a0fa2e --- /dev/null +++ b/docs/sources/whatsnew/whats-new-in-v10-2.md @@ -0,0 +1,476 @@ +--- +description: Feature and improvement highlights for Grafana v10.2 +keywords: + - grafana + - new + - documentation + - '10.2' + - release notes +labels: +products: + - cloud + - enterprise + - oss +title: What's new in Grafana v10.2 +weight: -39 +--- + +# What’s new in Grafana v10.2 + +Welcome to Grafana 10.2! Read on to learn about changes to dashboards and visualizations, data sources, security and authentication, and more. We’re particularly excited about the addition of generative AI features for dashboards, a new kind of basic role, and improvements to visualization transformations. + +For even more detail about all the changes in this release, refer to the [changelog](https://github.com/grafana/grafana/blob/master/CHANGELOG.md). For the specific steps we recommend when you upgrade to v10.2, check out our [Upgrade Guide](https://grafana.com/docs/grafana//upgrade-guide/upgrade-v10.2/). + + + + + +## Share your dashboard with the world: Public dashboards are generally available + + + +_Generally available in all editions of Grafana_ + +Public dashboards allow you to share your visualizations and insights with a broader audience without the requirement of a login. You can effortlessly use our current sharing model and create a public dashboard URL to share with anyone using the generated public URL link. To learn more, refer to the [Public dashboards documentation](https://grafana.com/docs/grafana//dashboards/dashboard-public/), as well as the following video demo: + +{{< video-embed src="/media/docs/grafana/dashboards/public-dashboards-demo.mp4" >}} + +## Navigate lengthy, mixed data in Explore with Content Outline + + + +_Generally available in all editions of Grafana_ + +Introducing Content Outline in Grafana Explore. It's easy to lose track of your place when you're running complex mixed queries or switching between logs and traces. Content outline is our first step towards seamless navigation from log lines to traces and back to queries, ensuring quicker searches while preserving context. Experience efficient, contextual investigations with this update in Grafana Explore. To learn more, refer to the [Content outline documentation](https://grafana.com/docs/grafana//explore/#content-outline), as well as the following video demo. + +{{< video-embed src="/media/docs/grafana/explore/content-outline-demo.mp4" >}} + +## Correlations + +Grafana Correlations is a new public preview feature you can use to establish links from any data source query to any other, carrying forward data like namespace, host, or label values. This is extremely powerful for performing root cause analysis with a diverse set of data sources. For more information, refer to [the documentation](https://grafana.com/docs/grafana//administration/correlations/). + +### Create Correlations the easy way in Grafana Explore + + + +_Available in public preview in all editions of Grafana_ + +Creating correlations has just become easier. Try out our new correlations editor in Explore by selecting the **+ Add > Add correlation** option from the top bar or from the command palette. The editor shows all possible places where you can place data links and guides you through building and testing target queries. For more information, refer to [the documentation](https://grafana.com/docs/grafana//administration/correlations/). + +To try out **Correlations**, enable the `correlations` [feature toggle](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles/#preview-feature-toggles). + +{{< figure src="/media/docs/grafana/correlations-explore-editor-10-2.png" max-width="750px" caption="Create a correlation with variables from within Grafana Explore" >}} + +### Create correlations for provisioned data sources + + + +_Available in public preview in all editions of Grafana_ + +In previous versions of Grafana, if a data source was provisioned, the only way to add correlations to it was also with provisioning. Now, that's no longer the case, and you can easily create new correlations mixing both methods—using the **Administration** page or provisioning. For more information, refer to [the documentation](https://grafana.com/docs/grafana//administration/correlations/). + +To try out **Correlations**, enable the `correlations` [feature toggle](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles/#preview-feature-toggles). + +## Dashboards and visualizations + +### Use AI to generate titles, descriptions, and change summaries + +_Available in public preview in all editions of Grafana_ + + + + +You can now use generative AI to assist you in your Grafana dashboards. So far generative AI can help you with the following tasks: + +- **Generate panel and dashboard titles and descriptions** - You can now generate a title and description for your panel or dashboard based on the data you've added to it. This is useful when you want to quickly visualize your data and don't want to spend time coming up with a title or description. +- **Generate dashboard save changes summary** - You can now generate a summary of the changes you've made to a dashboard when you save it. This is great for effortlessly tracking the history of a dashboard. + +To enable these features, you must first enable the `dashgpt` [feature toggle](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles/#experimental-feature-toggles). Then install and configure Grafana's LLM app plugin. For more information, refer to the [Grafana LLM app plugin documentation](https://grafana.com/docs/grafana-cloud/alerting-and-irm/machine-learning/llm-plugin/). + +When enabled, look for the **✨ Auto generate** option next to the **Title** and **Description** fields in your panels and dashboards, or when you press the **Save** button. + +{{< figure src="/media/docs/grafana/dashboards/auto-generate-description-10-2.gif" max-width="750px" caption="Auto-generate a panel description using AI" >}} + +### Find your dashboard faster with the new Dashboards browse screen + + + +_Generally available in all editions of Grafana_ + +The new browse screen for dashboards features a more compact design, making it easier to navigate, search for, and manage your folders and dashboards. The new interface also has many performance improvements, especially for instances with a large number of folders and dashboards. + +To make using folders easier and more consistent, there's no longer a special **General** folder. Dashboards without a folder, or dashboards previously in the **General** folder, are now shown at the root level. + +To try it out, go to the **Dashboards** section of your Grafana instance. + +{{< video-embed src="/media/docs/grafana/2023-09-11-New-Browse-Dashboards-Enablement-Video.mp4" >}} + +### Create interactive buttons in canvas visualizations + +_Available in public preview in all editions of Grafana_ + + + + +You can now add buttons to your canvas visualizations. Buttons can be configured to call an API endpoint. This pushes Grafana's capabilities to new heights, allowing you to create interactive dashboards that can be used to control external systems. + +To learn more, refer to our [Canvas button element documentation](https://grafana.com/docs/grafana//panels-visualizations/visualizations/canvas/#button). + +{{< video-embed src="/media/docs/grafana/2023-20-10-Canvas-Button-Element-Enablement-Video.mp4" max-width="750px" caption="Canvas button element demo" >}} + +### Zoom in on the y-axis of the time series and candlestick visualizations + +_Generally available in all editions of Grafana_ + + + + +You can now zoom in on the y-axis of your time series and candlestick visualizations. This is useful when you want to focus on a specific range of values. To zoom in on the y-axis on supported visualizations, hold the Shift key while clicking and dragging; double-click to reset the zoom. + +{{< video-embed src="/media/docs/grafana/screen-recording-10-2-y-axis-zoom-demo.mp4" max-width="750px" caption="Y-axis zooming demo" >}} + +### Calculate visualization min/max individually per field + + + +_Generally available in all editions of Grafana_ + +When visualizing multiple fields with a wide spread of values, calculating the min or max value of the visualization based on all fields can hide useful details. + +{{< figure src="/media/docs/grafana/panels-visualizations/globalminmax.png" max-width="300px" caption="Stat visualization with min/max calculated from all fields" >}} + +In this example in the stat visualization, it's hard to get an idea of how the values of each series relate to the historical values of that series. The threshold of 10% is exceeded by the A-series even though the A-series is below 10% of its historical maximum. + +Now, you can automatically calculate the min or max of each visualized field based on the lowest and highest value of the individual field. This setting is available in the standard options of most visualizations. + +{{< figure src="/media/docs/grafana/panels-visualizations/localminmax.png" max-width="300px" caption="Stat visualization with min/max calculated per field" >}} + +In this example, using the same data, with the min and max calculated for each individual field, we get a much better understanding of how the current value relates to the historical values. The A-series no longer exceeds the 10% threshold; in fact, it's now clear that it's at a historical low. + +This isn't only useful in the stat visualization—gauge, bar gauge, and status history visualizations, table cells formatted by thresholds, and gauge table cells all benefit from this addition. + +### Data visualization quality of life improvements + +_Generally available in all editions of Grafana_ + + + + +We've made a number of smaller improvements to the data visualization experience in Grafana. + +#### Geomap marker symbol alignment options + +You can now offset geomap marker symbols from the underlying data point. + +{{< figure src="/media/docs/grafana/gif-grafana-10-2-geomap-marker-symbol-alignment.gif" max-width="750px" caption="Geomap marker symbol alignment" >}} + +#### Gauge visualization overflow support + +You can now visualize gauges in vertical and horizontal orientations with overflow. This resolves an issue where the design would break when the number of gauges exceeded the available space. + +{{< figure src="/media/docs/grafana/gif-grafana-10-2-gauge-overflow.gif" max-width="750px" caption="Gauge overflow" >}} + +#### Bar chart axes improvements + +You can now center bar chart axes on zero and configure axes border and color settings. + +{{< figure src="/media/docs/grafana/screenshot-grafana-10-2-bar-chart-axes-improvements.png" max-width="750px" caption="Bar chart improvements" >}} + +## Data sources and querying + +### Tempo data source + +We've placed special focus on the Tempo data source over the past couple of months with new features, query performance improvements, and a better query experience. + +#### Compute RED metrics over spans aggregated by attribute with the "Aggregate By" Search option + + + +_Experimental in all editions of Grafana_ + +Requires Tempo or Grafana Enterprise Traces (GET) v2.2 or greater. + +We've added an **Aggregate By** option to the [TraceQL query editor](https://grafana.com/docs/grafana//datasources/tempo/query-editor/traceql-search/#write-traceql-queries-using-search) to leverage Tempo's [metrics summary API](https://grafana.com/docs/tempo//api_docs/metrics-summary/). You can calculate RED metrics (total span count, percent erroring spans, and latency information) for spans of `kind=server` received in the last hour that match your filter criteria, grouped by whatever attributes you specify. + +This feature is disabled by default. To enable it, use the `metricsSummary` [experimental feature toggle](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles/#experimental-feature-toggles). + +For more information, refer to the [documentation](https://grafana.com/docs/grafana//datasources/tempo/query-editor/traceql-search/#optional-use-aggregate-by). + +{{< figure src="/media/docs/tempo/metrics-summary-10-2.png" caption="Aggregate by" >}} + +#### Query traces more easily with the Improved TraceQL editor + + + +_Generally available in all editions of Grafana_ + +The [TraceQL query editor](https://grafana.com/docs/tempo/latest/traceql/#traceql-query-editor) has been improved to facilitate the creation of TraceQL queries. In particular, it now features improved autocompletion, syntax highlighting, and error reporting. + +{{< video-embed src="/media/docs/tempo/screen-recording-grafana-10.2-traceql-query-editor-improvements.mp4" >}} + +#### Group multiple spansets per trace + + + +_Generally available in all editions of Grafana_ + +The [TraceQL query editor](https://grafana.com/docs/tempo//traceql/#traceql-query-editor) has been improved to facilitate the grouping of multiple spans per trace in TraceQL queries. For example, when `by(resource.service.name)` is added to your TraceQL query, it will group the spans in each trace by `resource.service.name`. + +{{< figure src="/media/docs/tempo/multiple-spansets-per-trace-10-2.png" max-width="750px" caption="Multiple spansets per trace" >}} + +#### Create query-type template variables for the Tempo data source + + + +_Generally available in all editions of Grafana_ + +The Tempo data source now supports query-type template variables. With this update, you can create variables for which the values are a list of attribute names or attribute values seen on spans received by Tempo. + +To learn more, refer to the following video demo, as well as the [Grafana Variables documentation](/docs/grafana/next/dashboards/variables/). + +{{< video-embed src="/media/docs/tempo/screen-recording-grafana-10.2-tempo-query-type-template-variables.mp4" >}} + +### SAP HANA®: Configure your data source with tenant database instance name and number + + + +_Generally available in Grafana Enterprise and Grafana Cloud_ + +The SAP HANA® data source now supports tenant database connections by using the database name and/or instance number. This is helpful because these are less likely to change than the port for your database. For more information, refer to our [SAP HANA® configuration documentation](/docs/plugins/grafana-saphana-datasource/latest/#configuration). + +{{< video-embed src="/media/docs/sap-hana/tenant.mp4" >}} + +### Datadog: Aggregate logs to compute metrics and time series + + + +_Generally available in Grafana Enterprise and Grafana Cloud_ + +The Datadog data source now supports log aggregation. This feature helps aggregate logs/events into buckets and compute metrics and time series. For more information, refer to [Datadog log aggregation](/docs/plugins/grafana-datadog-datasource/latest#logs-analytics--aggregation). + +{{< video-embed src="/media/docs/datadog/datadog-log-aggregation.mp4" >}} + +### Datadog: Rate-limit requests from the Datadog data source + + + +_Generally available in Grafana Enterprise and Grafana Cloud_ + +In the Datadog data source, you can now block API requests for metric queries based on upstream rate limits. With this update, you can set a rate limit percentage at which the plugin stops sending queries. + +To learn more, refer to [Datadog data source settings](/docs/plugins/grafana-datadog-datasource/latest#configure-the-data-source), as well as the following video demo. + +{{< video-embed src="/media/docs/datadog/datadog-rate-limit.mp4" >}} + +## Transformations + +As our work on improving the user experience of transforming data continues, we've also been adding new capabilities to transformations. + +### Use dashboard variables in transformations + + + +_Experimental in all editions of Grafana_ + +Previously, the only transformation that supported [dashboard variables](https://grafana.com/docs/grafana//dashboards/variables/) was the **Add field from calculation** transformation. We've now extended the support for variables to the **Filter by value**, **Create heatmap**, **Histogram**, **Sort by**, **Limit**, **Filter by name**, and **Join by field** transformations. We've also made it easier to find the correct dashboard variable by displaying available variables in the fields that support them, either in the drop-down or as a suggestion when you type **$** or press Ctrl + Space: + +{{< figure src="/media/docs/grafana/transformations/completion.png" caption="Input with dashboard variable suggestions" >}} + +### New modes for the Add field from calculation transformation + + + +_Generally available in all editions of Grafana_ + +The **Add field from calculation** transformation has been updated. + +**Unary operation** is a new mode that lets you apply mathematical operations to a field. The currently supported operations are: + +- **Absolute value (abs)** - Returns the absolute value of a given expression. It represents its distance from zero as a positive number. +- **Natural exponential (exp)** - Returns _e_ raised to the power of a given expression. +- **Natural logarithm (ln)** - Returns the natural logarithm of a given expression. +- **Floor (floor)** - Returns the largest integer less than or equal to a given expression. +- **Ceiling (ceil)** - Returns the smallest integer greater than or equal to a given expression. + +{{< figure src="/media/docs/grafana/transformations/unary-operation.png" >}} + +Also, **Row index** can now show the index as a percentage. + +Learn more in the [Add field from calculation documentation](https://grafana.com/docs/grafana//panels-visualizations/query-transform-data/transform-data/#add-field-from-calculation). + +### Format strings with transformations + + + +_Experimental in all editions of Grafana_ + +With the new **Format string** transformation, you can manipulate string fields to improve how they're displayed. The currently supported operations are: + +- **Change case** changes the case of your string to upper case, lower case, sentence case, title case, pascal case, camel case, or snake case. +- **Trim** removes white space characters at the start and end of your string. +- **Substring** selects a part of your string field. + +Learn more in the [Format string documentation](https://grafana.com/docs/grafana//panels-visualizations/query-transform-data/transform-data/#format-string). + +### See which transformations will work with your data + + + +_Available in public preview in all editions of Grafana_ + +We've added initial support to detect situations in which various transformations won't work appropriately based on current data. Previously, selecting the appropriate transformation and configuring it correctly required a process of trial and error or already knowing how a given transformation worked. Now, transformations that we've detected can't be used are shaded in the interface to indicate this, along with a helpful message explaining why. + +{{< figure src="/media/docs/grafana/transformations/disabled-transformation.png" caption="Transformation that has been disabled because it doesn't have the necessary data" >}} + +If you have the `transformationsRedesign` feature flag set, you'll be able to access this functionality right away. If you'd like to try it, enable this feature flag in your [Grafana configuration](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/#feature_toggles). + +### Choose your timezome in the Format time and Convert field type transformations + + + +_Generally available in all editions of Grafana_ + +We've added support for setting timezones manually when formatting times as strings using the **Format time** and **Convert field type** transformations. This allows times to be formatted relative to any timezone across the globe. + +{{< figure src="/media/docs/grafana/transformations/format-timezone.png" caption="Timezone support in the Format time transformation" >}} + +## Alerting + +### Grafana OnCall integration for Alerting + + + +_Generally available in all editions of Grafana_ + +Use the Grafana Alerting - Grafana OnCall integration to effortlessly connect alerts generated by Grafana Alerting with Grafana OnCall. From there, you can route them according to defined escalation chains and schedules. + +To learn more, refer to the [Grafana OnCall integration for Alerting documentation](https://grafana.com/docs/grafana//alerting/alerting-rules/manage-contact-points/configure-oncall/). + +### Export alerting resources to Terraform + + + +_Generally available in all editions of Grafana_ + +Export your alerting resources, such as alert rules, contact points, and notification policies as Terraform resources. A new “Modify export” mode for alert rules enables you to edit provisioned alert rules and export a modified version. + +### Additional contact points for external Alertmanager + + + +_Generally available in Grafana Open Source and Enterprise_ + +We've added support for the Microsoft Teams contact points when using an external Alertmanager. + +## Authentication and authorization + +### No basic role + + + +_Generally available in Grafana Enterprise and Grafana Cloud_ + +We're excited to introduce the "No basic role," a new basic role with no permissions. A basic role in Grafana dictates the set of actions a user or entity can perform, known as permissions. This new role is especially beneficial if you're aiming for tailored, customized RBAC permissions for your service accounts or users. You can set this as a basic role through the API or UI. + +Previously, permissions were granted based on predefined sets of capabilities. Now, with the "No basic role," you have the flexibility to be even more granular. + +For more details on basic roles and permissions, refer to the [documentation](https://grafana.com/docs/grafana//administration/roles-and-permissions/). + +### New service account permissions + +[Service accounts](https://grafana.com/docs/grafana//administration/service-accounts/) allow you to create tokens to access Grafana's API and dashboards. Service accounts are a powerful tool for authenticating with Grafana's API and accessing data sources. However, without proper access controls, service accounts can pose a security risk to your Grafana instance. In Grafana 10.2, we've added new tools to limit service accounts to just the resources they need to access. + +#### Add dashboard and folder permissions to service accounts + + + +_Generally available in all editions of Grafana_ + +In this release, we've added the ability to assign dashboard and folder permissions to service accounts. +This means that you can now create a service account that can be used to access a specific dashboard and nothing else. + +This is useful if you want to limit the access service accounts have to your Grafana instance. + +Learn more in our [dashboard and folder permissions documentation](https://grafana.com/docs/grafana//administration/user-management/manage-dashboard-permissions/#manage-dashboard-permissions). + +#### Add data source permissions to service accounts + + + +_Generally available in Grafana Cloud and Grafana Enterprise_ + +Grafana 10.2 also introduces the ability to assign _data source_ permissions to service accounts, for Grafana CLoud and Enterprise users. +With this feature, you can create a service account that has access to a specific data source and nothing else. +This is useful in scenarios where you want to limit the access service accounts have to your Grafana instance. + +For example, imagine you have a team of developers who need to access a specific data source to develop a new feature. +Instead of giving them full access to your Grafana instance, you can create a service account that has access only to that data source. +This way, you can limit the potential damage that could be caused by a compromised service account. + +Learn more in our [data source permissions documentation](https://grafana.com/docs/grafana//administration/data-source-management/#data-source-permissions). + +{{< figure src="/media/docs/grafana/screenshot-grafana-10-2-sa-managed-permissions.png" max-width="600px" caption="Data source permissions in 10.2" >}} + +### Role mapping support for Google OIDC + + + +_Generally available in all editions of Grafana_ + +You can now map Google groups to Grafana organizational roles when using Google OIDC. +This is useful if you want to limit the access users have to your Grafana instance. + +We've also added support for controlling allowed groups when using Google OIDC. + +Refer to the [Google Authentication documentation](http://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/google/) to learn how to use these new options. + +### Configure refresh token handling separately for OAuth providers + + + +_Available in public preview in all editions of Grafana_ + +With Grafana v9.3, we introduced a [feature toggle](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles/) called `accessTokenExpirationCheck`. It improves the security of Grafana by checking the expiration of the access token and automatically refreshing the expired access token when a user is logged in using one of the OAuth providers. + +With the current release, we've introduced a new configuration option for each OAuth provider called `use_refresh_token` that allows you to configure whether the particular OAuth integration should use refresh tokens to automatically refresh access tokens when they expire. In addition, to further improve security and provide secure defaults, `use_refresh_token` is enabled by default for providers that support either refreshing tokens automatically or client-controlled fetching of refresh tokens. It's enabled by default for the following OAuth providers: `AzureAD`, `GitLab`, `Google`. + +For more information on how to set up refresh token handling, please refer to [the documentation of the particular OAuth provider.](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/). + +{{% admonition type="note" %}} +The `use_refresh_token` configuration must be used in conjunction with the `accessTokenExpirationCheck` [feature toggle](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles/). If you disable the `accessTokenExpirationCheck` feature toggle, Grafana won't check the expiration of the access token and won't automatically refresh the expired access token, even if the `use_refresh_token` configuration is set to `true`. + +The `accessTokenExpirationCheck` feature toggle will be removed in Grafana v10.3. +{{% /admonition %}} + +### Permission validation on custom role creation and update + + + +_Generally available in Grafana Enterprise and Grafana Cloud_ + +With the current release, we enabled RBAC permission validation (`rbac.permission_validation_enabled` setting) by default. This means that the permissions provided in the request during custom role creation or update are validated against the list of [available permissions and their scopes](https://grafana.com/docs/grafana//administration/roles-and-permissions/access-control/custom-role-actions-scopes/#action-definitions). If the request contains a permission that is not available or the scope of the permission is not valid, the request is rejected with an error message. + +## Recorded queries: Record multiple metrics from a single query + + + +_Generally available in Grafana Enterprise and Grafana Cloud_ + +Recorded queries provide a way to take a _static_ number, (for example, the number of GitHub issues open at a given time, or the number of rows in a database table) and record it periodically as a Prometheus metric. This is great for tracking numbers over time for quick querying later. Previously, recorded queries were limited to a single series, so you needed to narrow your query down to a single number in order to record it. Now, you can record multiple metrics with a single recorded query, which makes them more powerful _and_ easier to create and manage. + + From bbcc81405b1262c2c1d2dfc234e2722158a2a85b Mon Sep 17 00:00:00 2001 From: Kat Yang <69819079+yangkb09@users.noreply.github.com> Date: Tue, 24 Oct 2023 13:01:04 -0400 Subject: [PATCH 040/180] Chore: Add CI Pipeline to generate Grafana's OpenAPI specification (#75393) * chore: move over initial changes from sofia's pr #58029 * chore: remove go_image * chore: begin removing edition, remove unused imports * chore: remove edition from swagger_gen.star and generate .drone.yml * chore: regen drone.yml * fix: fix order of load statements * fix: try #2 fix order of load statements * linter fixes * chore: add doc comment explaining purpose of new clone_pr_branch step * fix: add placeholder documentation for ver_mode arg * attempt #1 to import and use clone_enterprise_step_pr * Update scripts/drone/pipelines/swagger_gen.star Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * attempt #2 to import and use clone_enterprise_step_pr * fix: fix drone lint err invalid or unknown step dependency * fix: regen hmac to make drone run * fix: fix drone lint err * fix: update swagger-clean cmd in step * attempt to return non zero exit code * test to see if pipeline fails * fix: add git to clone pr branch step * fix: add git and make to swagger-gen step * try to rerun drone * debug: figure out why cannot find make swagger-clean * debug: see if cd grafana/grafana fixes things * debug: undo cd grafana/grafana * debug: add more logging statements * debug: try and remove cd grafana in swagger-gen * debug: removed grafana after clone statement; add debug before cloning * fix: remove disable clone * regen specs to see if swagger-gen step passes now * add descriptive error message to swagger-gen step * remove api-spec.json from .gitignore * revert backend change, regen spec * add back backend change, regen specs * Update scripts/drone/pipelines/swagger_gen.star Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Update scripts/drone/pipelines/swagger_gen.star Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * revert gitignore change, set enterprise source to drone source branch * chore: remove unused variable, change committish var name to source * testing functionality: make a new BE change without gen spec, pipeline should fail * test functionality: does removing err msg cause step to fail properly * test functionality: add back err msg and && after * chore: remove debug statements from swagger gen step * chore: remove debug lines from clone_pr_branch step * test functionality: regen specs, swagger_gen step should pass * test funcionality: regen specs again ???? * chore: update swagger-gen step err msg * test functionality: make BE change dont regen spec, swagger gen should fail * test functionality: regen the specs, swagger-gen should pass * chore: revert test BE change, regen spec * chore: remove unused clone step * chore: regen drone.yml --------- Co-authored-by: Timur Olzhabayev Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> --- .drone.yml | 62 ++++++++++++++++++++- public/api-merged.json | 71 ++++++++++++++++-------- public/openapi3.json | 71 ++++++++++++++++-------- scripts/drone/events/pr.star | 8 +++ scripts/drone/pipelines/swagger_gen.star | 56 +++++++++++++++++++ 5 files changed, 219 insertions(+), 49 deletions(-) create mode 100644 scripts/drone/pipelines/swagger_gen.star diff --git a/.drone.yml b/.drone.yml index ead82b92296..385229cdfb6 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1132,6 +1132,66 @@ volumes: clone: retries: 3 depends_on: [] +image_pull_secrets: +- dockerconfigjson +kind: pipeline +name: pr-swagger-gen +node: + type: no-parallel +platform: + arch: amd64 + os: linux +services: [] +steps: +- commands: + - apk add --update curl jq bash + - is_fork=$(curl "https://$GITHUB_TOKEN@api.github.com/repos/grafana/grafana/pulls/$DRONE_PULL_REQUEST" + | jq .head.repo.fork) + - if [ "$is_fork" != false ]; then return 1; fi + - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" + grafana-enterprise + - cd grafana-enterprise + - if git checkout ${DRONE_SOURCE_BRANCH}; then echo "checked out ${DRONE_SOURCE_BRANCH}"; + elif git checkout main; then echo "git checkout main"; else git checkout main; + fi + environment: + GITHUB_TOKEN: + from_secret: github_token + image: alpine/git:2.40.1 + name: clone-enterprise +- commands: + - apk add --update git make + - make swagger-clean && make openapi3-gen + - for f in public/api-merged.json public/openapi3.json; do git add $f; done + - if [ -z "$(git diff --name-only --cached)" ]; then echo "Everything seems up to + date!"; else echo "Please ensure the branch is up-to-date, then regenerate the + specification by running make swagger-clean && make openapi3-gen" && return 1; + fi + depends_on: + - clone-enterprise + environment: + GITHUB_TOKEN: + from_secret: github_token + image: golang:1.20.10-alpine + name: swagger-gen +trigger: + event: + - pull_request + paths: + exclude: + - docs/** + - '*.md' + include: + - pkg/** +type: docker +volumes: +- host: + path: /var/run/docker.sock + name: docker +--- +clone: + retries: 3 +depends_on: [] environment: EDITION: oss image_pull_secrets: @@ -4607,6 +4667,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 93de8a710e23d3f1d31860f9eed34cd841b0a0eb48637971de4e8ce60a7c3df1 +hmac: 29a933affceb9cc39b285d936de9e6327deedbb80f1285fa645d596f89ede442 ... diff --git a/public/api-merged.json b/public/api-merged.json index 69a907542a6..326dfb0aa18 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -639,30 +639,6 @@ } } }, - "/admin/ldap-sync-status": { - "get": { - "description": "You need to have a permission with action `ldap.status:read`.", - "tags": [ - "ldap_debug" - ], - "summary": "Returns the current state of the LDAP background sync integration.", - "operationId": "getSyncStatus", - "responses": { - "200": { - "$ref": "#/responses/getSyncStatusResponse" - }, - "401": { - "$ref": "#/responses/unauthorisedError" - }, - "403": { - "$ref": "#/responses/forbiddenError" - }, - "500": { - "$ref": "#/responses/internalServerError" - } - } - } - }, "/admin/ldap/reload": { "post": { "security": [ @@ -15166,6 +15142,53 @@ } } }, + "JSONWebKey": { + "type": "object", + "title": "JSONWebKey represents a public or private key in JWK format.", + "properties": { + "Algorithm": { + "description": "Key algorithm, parsed from `alg` header.", + "type": "string" + }, + "CertificateThumbprintSHA1": { + "description": "X.509 certificate thumbprint (SHA-1), parsed from `x5t` header.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8" + } + }, + "CertificateThumbprintSHA256": { + "description": "X.509 certificate thumbprint (SHA-256), parsed from `x5t#S256` header.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8" + } + }, + "Certificates": { + "description": "X.509 certificate chain, parsed from `x5c` header.", + "type": "array", + "items": { + "$ref": "#/definitions/Certificate" + } + }, + "CertificatesURL": { + "$ref": "#/definitions/URL" + }, + "Key": { + "description": "Cryptographic key, can be a symmetric or asymmetric key." + }, + "KeyID": { + "description": "Key identifier, parsed from `kid` header.", + "type": "string" + }, + "Use": { + "description": "Key use, parsed from `use` header.", + "type": "string" + } + } + }, "Json": { "type": "object" }, diff --git a/public/openapi3.json b/public/openapi3.json index 5a66d55d215..d577849398e 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -6234,6 +6234,53 @@ "title": "JSONWebKey represents a public or private key in JWK format.", "type": "object" }, + "JSONWebKey": { + "properties": { + "Algorithm": { + "description": "Key algorithm, parsed from `alg` header.", + "type": "string" + }, + "CertificateThumbprintSHA1": { + "description": "X.509 certificate thumbprint (SHA-1), parsed from `x5t` header.", + "items": { + "format": "uint8", + "type": "integer" + }, + "type": "array" + }, + "CertificateThumbprintSHA256": { + "description": "X.509 certificate thumbprint (SHA-256), parsed from `x5t#S256` header.", + "items": { + "format": "uint8", + "type": "integer" + }, + "type": "array" + }, + "Certificates": { + "description": "X.509 certificate chain, parsed from `x5c` header.", + "items": { + "$ref": "#/components/schemas/Certificate" + }, + "type": "array" + }, + "CertificatesURL": { + "$ref": "#/components/schemas/URL" + }, + "Key": { + "description": "Cryptographic key, can be a symmetric or asymmetric key." + }, + "KeyID": { + "description": "Key identifier, parsed from `kid` header.", + "type": "string" + }, + "Use": { + "description": "Key use, parsed from `use` header.", + "type": "string" + } + }, + "title": "JSONWebKey represents a public or private key in JWK format.", + "type": "object" + }, "Json": { "type": "object" }, @@ -12481,30 +12528,6 @@ ] } }, - "/admin/ldap-sync-status": { - "get": { - "description": "You need to have a permission with action `ldap.status:read`.", - "operationId": "getSyncStatus", - "responses": { - "200": { - "$ref": "#/components/responses/getSyncStatusResponse" - }, - "401": { - "$ref": "#/components/responses/unauthorisedError" - }, - "403": { - "$ref": "#/components/responses/forbiddenError" - }, - "500": { - "$ref": "#/components/responses/internalServerError" - } - }, - "summary": "Returns the current state of the LDAP background sync integration.", - "tags": [ - "ldap_debug" - ] - } - }, "/admin/ldap/reload": { "post": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `ldap.config:reload`.", diff --git a/scripts/drone/events/pr.star b/scripts/drone/events/pr.star index cbd425efcf6..d7b75151197 100644 --- a/scripts/drone/events/pr.star +++ b/scripts/drone/events/pr.star @@ -32,6 +32,10 @@ load( "scripts/drone/pipelines/shellcheck.star", "shellcheck_pipeline", ) +load( + "scripts/drone/pipelines/swagger_gen.star", + "swagger_gen", +) load( "scripts/drone/pipelines/test_backend.star", "test_backend", @@ -137,6 +141,10 @@ def pr_pipelines(): ), docs_pipelines(ver_mode, trigger_docs_pr()), shellcheck_pipeline(), + swagger_gen( + get_pr_trigger(include_paths = ["pkg/**"]), + ver_mode, + ), integration_benchmarks( prefix = ver_mode, ), diff --git a/scripts/drone/pipelines/swagger_gen.star b/scripts/drone/pipelines/swagger_gen.star new file mode 100644 index 00000000000..755b84ee4cb --- /dev/null +++ b/scripts/drone/pipelines/swagger_gen.star @@ -0,0 +1,56 @@ +""" +This module returns all pipelines used in OpenAPI specification generation of Grafana HTTP APIs +""" + +load( + "scripts/drone/steps/lib.star", + "clone_enterprise_step_pr", +) +load( + "scripts/drone/utils/images.star", + "images", +) +load( + "scripts/drone/utils/utils.star", + "pipeline", +) +load( + "scripts/drone/vault.star", + "from_secret", +) + +def swagger_gen_step(ver_mode): + if ver_mode != "pr": + return None + + return { + "name": "swagger-gen", + "image": images["go"], + "environment": { + "GITHUB_TOKEN": from_secret("github_token"), + }, + "commands": [ + "apk add --update git make", + "make swagger-clean && make openapi3-gen", + "for f in public/api-merged.json public/openapi3.json; do git add $f; done", + 'if [ -z "$(git diff --name-only --cached)" ]; then echo "Everything seems up to date!"; else echo "Please ensure the branch is up-to-date, then regenerate the specification by running make swagger-clean && make openapi3-gen" && return 1; fi', + ], + "depends_on": [ + "clone-enterprise", + ], + } + +def swagger_gen(trigger, ver_mode, source = "${DRONE_SOURCE_BRANCH}"): + test_steps = [ + clone_enterprise_step_pr(source = source), + swagger_gen_step(ver_mode = ver_mode), + ] + + p = pipeline( + name = "{}-swagger-gen".format(ver_mode), + trigger = trigger, + services = [], + steps = test_steps, + ) + + return p From d75886c3ac853e52d9158034cfc981738391640b Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Tue, 24 Oct 2023 20:11:37 +0200 Subject: [PATCH 041/180] Chore: Remove default metadata key from influxdb sql config page (#76994) Remove default metadata key --- .../influxdb/components/editor/config/InfluxSQLConfig.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/components/editor/config/InfluxSQLConfig.tsx b/public/app/plugins/datasource/influxdb/components/editor/config/InfluxSQLConfig.tsx index 519b0502ece..501cc4cd7f2 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/config/InfluxSQLConfig.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/config/InfluxSQLConfig.tsx @@ -52,7 +52,7 @@ export const InfluxSqlConfig = (props: Props) => { const existingMetadata: MetadataState = jsonData?.metadata?.length ? jsonData?.metadata?.map((md) => ({ key: Object.keys(md)[0], value: Object.values(md)[0] })) - : [{ key: 'bucket-name', value: '' }]; + : [{ key: '', value: '' }]; const [metaDataArr, setMetaData] = useState(existingMetadata); useEffect(() => { @@ -101,7 +101,7 @@ export const InfluxSqlConfig = (props: Props) => { type="text" value={metaDataArr[i]?.key || ''} placeholder="key" - onChange={(e) => onKeyChange(e.currentTarget.value, metaDataArr, i, setMetaData)} + onChange={(e) => onKeyChange(e.currentTarget.value.trim(), metaDataArr, i, setMetaData)} > @@ -112,7 +112,7 @@ export const InfluxSqlConfig = (props: Props) => { type="text" value={metaDataArr[i]?.value?.toString() ?? ''} placeholder="value" - onChange={(e) => onValueChange(e.currentTarget.value, metaDataArr, i, setMetaData)} + onChange={(e) => onValueChange(e.currentTarget.value.trim(), metaDataArr, i, setMetaData)} > {i + 1 >= metaDataArr.length && ( From ba384d29f632ca1c04c2736f0f00d965b9c61f5c Mon Sep 17 00:00:00 2001 From: George Robinson Date: Tue, 24 Oct 2023 19:31:03 +0100 Subject: [PATCH 042/180] Alerting: Fix order of the Alerting docs (#77084) --- docs/sources/alerting/alerting-rules/_index.md | 2 +- docs/sources/alerting/fundamentals/_index.md | 2 +- docs/sources/alerting/manage-notifications/_index.md | 2 +- docs/sources/alerting/monitor/_index.md | 2 +- docs/sources/alerting/set-up/_index.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/sources/alerting/alerting-rules/_index.md b/docs/sources/alerting/alerting-rules/_index.md index 06e60be604b..750b7c7c414 100644 --- a/docs/sources/alerting/alerting-rules/_index.md +++ b/docs/sources/alerting/alerting-rules/_index.md @@ -13,7 +13,7 @@ labels: - oss menuTitle: Configure title: Configure Alerting -weight: 130 +weight: 120 --- # Configure Alerting diff --git a/docs/sources/alerting/fundamentals/_index.md b/docs/sources/alerting/fundamentals/_index.md index da63a9465be..5ed760bebd3 100644 --- a/docs/sources/alerting/fundamentals/_index.md +++ b/docs/sources/alerting/fundamentals/_index.md @@ -11,7 +11,7 @@ labels: - oss menuTitle: Introduction title: Introduction to Alerting -weight: 150 +weight: 100 --- # Introduction to Alerting diff --git a/docs/sources/alerting/manage-notifications/_index.md b/docs/sources/alerting/manage-notifications/_index.md index 0187cbecf3e..65c38e9d652 100644 --- a/docs/sources/alerting/manage-notifications/_index.md +++ b/docs/sources/alerting/manage-notifications/_index.md @@ -12,7 +12,7 @@ labels: - oss menuTitle: Manage title: Manage your alerts -weight: 160 +weight: 130 --- # Manage your alerts diff --git a/docs/sources/alerting/monitor/_index.md b/docs/sources/alerting/monitor/_index.md index 65f07463b56..cbd9853387a 100644 --- a/docs/sources/alerting/monitor/_index.md +++ b/docs/sources/alerting/monitor/_index.md @@ -14,7 +14,7 @@ labels: - oss menuTitle: Monitor title: Meta monitoring -weight: 200 +weight: 140 --- # Meta monitoring diff --git a/docs/sources/alerting/set-up/_index.md b/docs/sources/alerting/set-up/_index.md index 08a09450121..7418bba3ca6 100644 --- a/docs/sources/alerting/set-up/_index.md +++ b/docs/sources/alerting/set-up/_index.md @@ -8,7 +8,7 @@ labels: - oss menuTitle: Set up title: Set up Alerting -weight: 107 +weight: 110 --- # Set up Alerting From da34e76fa0a67ae694b87bb4fc20ab158d9b431a Mon Sep 17 00:00:00 2001 From: Ryan Crutchfield <30603182+rjcrutch@users.noreply.github.com> Date: Tue, 24 Oct 2023 13:21:02 -0600 Subject: [PATCH 043/180] Docs: Fixing Security Advisory URL (#75483) Update breaking-changes-v10-0.md Fixing broken URL to security CVE advisory --- docs/sources/breaking-changes/breaking-changes-v10-0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/breaking-changes/breaking-changes-v10-0.md b/docs/sources/breaking-changes/breaking-changes-v10-0.md index d5c5db5f8ce..ee3387a9f4b 100644 --- a/docs/sources/breaking-changes/breaking-changes-v10-0.md +++ b/docs/sources/breaking-changes/breaking-changes-v10-0.md @@ -192,7 +192,7 @@ We strongly recommend not doing this in case you are using Azure AD as an identi #### Learn more -- [CVE-2023-3128 Advisory](https://grafana.com/security/security-advisories/CVE-2023-3128) +- [CVE-2023-3128 Advisory](https://grafana.com/security/security-advisories/cve-2023-3128/) - [Enable email lookup]({{< relref "../setup-grafana/configure-security/configure-authentication/" >}}) ### The "Alias" field in the CloudWatch data source is removed From b0b033584e0a7cc59d23b993609cdad8a2811a90 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Wed, 25 Oct 2023 10:27:37 +0300 Subject: [PATCH 044/180] Chore: Update OpenAPI specs (#77107) --- public/api-enterprise-spec.json | 9 ++++ public/api-merged.json | 80 ++++++++++++++------------------- public/openapi3.json | 80 ++++++++++++++------------------- 3 files changed, 75 insertions(+), 94 deletions(-) diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index 75597c1c321..9701d0b0a76 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -6304,6 +6304,7 @@ "$ref": "#/definitions/Json" }, "folderId": { + "description": "Deprecated: use FolderUID instead", "type": "integer", "format": "int64" }, @@ -6533,6 +6534,10 @@ "type": "boolean", "example": false }, + "isExternal": { + "type": "boolean", + "example": false + }, "login": { "type": "string", "example": "sa-grafana" @@ -6584,6 +6589,10 @@ "type": "boolean", "example": false }, + "isExternal": { + "type": "boolean", + "example": false + }, "login": { "type": "string", "example": "sa-grafana" diff --git a/public/api-merged.json b/public/api-merged.json index 326dfb0aa18..2292da639b4 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -639,6 +639,30 @@ } } }, + "/admin/ldap-sync-status": { + "get": { + "description": "You need to have a permission with action `ldap.status:read`.", + "tags": [ + "ldap_debug" + ], + "summary": "Returns the current state of the LDAP background sync integration.", + "operationId": "getSyncStatus", + "responses": { + "200": { + "$ref": "#/responses/getSyncStatusResponse" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "403": { + "$ref": "#/responses/forbiddenError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, "/admin/ldap/reload": { "post": { "security": [ @@ -15142,53 +15166,6 @@ } } }, - "JSONWebKey": { - "type": "object", - "title": "JSONWebKey represents a public or private key in JWK format.", - "properties": { - "Algorithm": { - "description": "Key algorithm, parsed from `alg` header.", - "type": "string" - }, - "CertificateThumbprintSHA1": { - "description": "X.509 certificate thumbprint (SHA-1), parsed from `x5t` header.", - "type": "array", - "items": { - "type": "integer", - "format": "uint8" - } - }, - "CertificateThumbprintSHA256": { - "description": "X.509 certificate thumbprint (SHA-256), parsed from `x5t#S256` header.", - "type": "array", - "items": { - "type": "integer", - "format": "uint8" - } - }, - "Certificates": { - "description": "X.509 certificate chain, parsed from `x5c` header.", - "type": "array", - "items": { - "$ref": "#/definitions/Certificate" - } - }, - "CertificatesURL": { - "$ref": "#/definitions/URL" - }, - "Key": { - "description": "Cryptographic key, can be a symmetric or asymmetric key." - }, - "KeyID": { - "description": "Key identifier, parsed from `kid` header.", - "type": "string" - }, - "Use": { - "description": "Key use, parsed from `use` header.", - "type": "string" - } - } - }, "Json": { "type": "object" }, @@ -17948,6 +17925,7 @@ "$ref": "#/definitions/Json" }, "folderId": { + "description": "Deprecated: use FolderUID instead", "type": "integer", "format": "int64" }, @@ -18185,6 +18163,10 @@ "type": "boolean", "example": false }, + "isExternal": { + "type": "boolean", + "example": false + }, "login": { "type": "string", "example": "sa-grafana" @@ -18236,6 +18218,10 @@ "type": "boolean", "example": false }, + "isExternal": { + "type": "boolean", + "example": false + }, "login": { "type": "string", "example": "sa-grafana" diff --git a/public/openapi3.json b/public/openapi3.json index d577849398e..61f3dc35912 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -6234,53 +6234,6 @@ "title": "JSONWebKey represents a public or private key in JWK format.", "type": "object" }, - "JSONWebKey": { - "properties": { - "Algorithm": { - "description": "Key algorithm, parsed from `alg` header.", - "type": "string" - }, - "CertificateThumbprintSHA1": { - "description": "X.509 certificate thumbprint (SHA-1), parsed from `x5t` header.", - "items": { - "format": "uint8", - "type": "integer" - }, - "type": "array" - }, - "CertificateThumbprintSHA256": { - "description": "X.509 certificate thumbprint (SHA-256), parsed from `x5t#S256` header.", - "items": { - "format": "uint8", - "type": "integer" - }, - "type": "array" - }, - "Certificates": { - "description": "X.509 certificate chain, parsed from `x5c` header.", - "items": { - "$ref": "#/components/schemas/Certificate" - }, - "type": "array" - }, - "CertificatesURL": { - "$ref": "#/components/schemas/URL" - }, - "Key": { - "description": "Cryptographic key, can be a symmetric or asymmetric key." - }, - "KeyID": { - "description": "Key identifier, parsed from `kid` header.", - "type": "string" - }, - "Use": { - "description": "Key use, parsed from `use` header.", - "type": "string" - } - }, - "title": "JSONWebKey represents a public or private key in JWK format.", - "type": "object" - }, "Json": { "type": "object" }, @@ -9038,6 +8991,7 @@ "$ref": "#/components/schemas/Json" }, "folderId": { + "description": "Deprecated: use FolderUID instead", "format": "int64", "type": "integer" }, @@ -9274,6 +9228,10 @@ "example": false, "type": "boolean" }, + "isExternal": { + "example": false, + "type": "boolean" + }, "login": { "example": "sa-grafana", "type": "string" @@ -9325,6 +9283,10 @@ "example": false, "type": "boolean" }, + "isExternal": { + "example": false, + "type": "boolean" + }, "login": { "example": "sa-grafana", "type": "string" @@ -12528,6 +12490,30 @@ ] } }, + "/admin/ldap-sync-status": { + "get": { + "description": "You need to have a permission with action `ldap.status:read`.", + "operationId": "getSyncStatus", + "responses": { + "200": { + "$ref": "#/components/responses/getSyncStatusResponse" + }, + "401": { + "$ref": "#/components/responses/unauthorisedError" + }, + "403": { + "$ref": "#/components/responses/forbiddenError" + }, + "500": { + "$ref": "#/components/responses/internalServerError" + } + }, + "summary": "Returns the current state of the LDAP background sync integration.", + "tags": [ + "ldap_debug" + ] + } + }, "/admin/ldap/reload": { "post": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `ldap.config:reload`.", From f1ce73dc2ca340fc2f5c9181795a4a72fcfebef2 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Wed, 25 Oct 2023 09:32:28 +0200 Subject: [PATCH 045/180] Alerting: Fix NoRulesSplash being rendered for some seconds, fater creating a rule (#77048) * Fix NoRulesSplash being rendered for some seconds, fater creating a rule * Add ruler response/loading,dispatched,error to the logic in hasNoAlertRulesCreatedYet expression --- public/app/features/alerting/unified/RuleList.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx index 655b52a9f3d..a36a7eccff3 100644 --- a/public/app/features/alerting/unified/RuleList.tsx +++ b/public/app/features/alerting/unified/RuleList.tsx @@ -61,11 +61,23 @@ const RuleList = withErrorBoundary( ); const promRequests = Object.entries(promRuleRequests); + const rulerRequests = Object.entries(rulerRuleRequests); + const allPromLoaded = promRequests.every( ([_, state]) => state.dispatched && (state?.result !== undefined || state?.error !== undefined) ); + const allRulerLoaded = rulerRequests.every( + ([_, state]) => state.dispatched && (state?.result !== undefined || state?.error !== undefined) + ); + const allPromEmpty = promRequests.every(([_, state]) => state.dispatched && state?.result?.length === 0); + const allRulerEmpty = rulerRequests.every(([_, state]) => { + const rulerRules = Object.entries(state?.result ?? {}); + const noRules = rulerRules.every(([_, result]) => result?.length === 0); + return noRules && state.dispatched; + }); + const limitAlerts = hasActiveFilters ? undefined : LIMIT_ALERTS; // Trigger data refresh only when the RULE_LIST_POLL_INTERVAL_MS elapsed since the previous load FINISHED const [_, fetchRules] = useAsyncFn(async () => { @@ -85,7 +97,8 @@ const RuleList = withErrorBoundary( useInterval(fetchRules, RULE_LIST_POLL_INTERVAL_MS); // Show splash only when we loaded all of the data sources and none of them has alerts - const hasNoAlertRulesCreatedYet = allPromLoaded && allPromEmpty && promRequests.length > 0; + const hasNoAlertRulesCreatedYet = + allPromLoaded && allPromEmpty && promRequests.length > 0 && allRulerEmpty && allRulerLoaded; const combinedNamespaces: CombinedRuleNamespace[] = useCombinedRuleNamespaces(); const filteredNamespaces = useFilteredRules(combinedNamespaces, filterState); From 37dbf037de548e1e855c0a59697769d20a8b5b65 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Wed, 25 Oct 2023 10:16:17 +0100 Subject: [PATCH 046/180] Tracing: Improve frame type checking (#76898) Improve frame type checking --- .../model/transform-trace-data.test.ts | 52 ------------------- .../components/model/transform-trace-data.tsx | 2 +- .../explore/TraceView/utils/transform.test.ts | 45 ++++++++++++++++ .../explore/TraceView/utils/transform.ts | 11 +++- 4 files changed, 55 insertions(+), 55 deletions(-) create mode 100644 public/app/features/explore/TraceView/utils/transform.test.ts diff --git a/public/app/features/explore/TraceView/components/model/transform-trace-data.test.ts b/public/app/features/explore/TraceView/components/model/transform-trace-data.test.ts index 7ff8742e335..53d3f3b696d 100644 --- a/public/app/features/explore/TraceView/components/model/transform-trace-data.test.ts +++ b/public/app/features/explore/TraceView/components/model/transform-trace-data.test.ts @@ -143,58 +143,6 @@ describe('transformTraceData()', () => { expect(transformTraceData(traceData)).toEqual(null); }); - it('should return null for any span without a spanID', () => { - const traceData = { - traceID, - processes, - spans: [ - { - traceID, - operationName: 'rootOperation', - references: [ - { - refType: 'CHILD_OF', - traceID, - spanID: rootSpanID, - }, - ], - startTime, - duration, - tags: [], - processID: 'p1', - }, - ], - } as unknown as TraceResponse; - - expect(transformTraceData(traceData)).toEqual(null); - }); - - it('should return null for any span without a processID', () => { - const traceData = { - traceID, - processes, - spans: [ - { - traceID, - spanID: '41f71485ed2593e4', - operationName: 'rootOperation', - references: [ - { - refType: 'CHILD_OF', - traceID, - spanID: rootSpanID, - }, - ], - startTime, - duration, - tags: [], - }, - ], - } as unknown as TraceResponse; - - expect(transformTraceData(traceData)).toEqual(null); - }); - it('should return trace data with correct traceName based on root span with missing ref', () => { const traceData = { traceID, diff --git a/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx b/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx index fef8f4fcf8e..ccd1905bd77 100644 --- a/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx +++ b/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx @@ -74,7 +74,7 @@ export function orderTags(tags: TraceKeyValuePair[], topPrefixes?: string[]) { * generally requires. */ export default function transformTraceData(data: TraceResponse | undefined): Trace | null { - if (!data?.traceID || data?.spans.some((x) => !x.processID || !x.spanID)) { + if (!data?.traceID) { return null; } const traceID = data.traceID.toLowerCase(); diff --git a/public/app/features/explore/TraceView/utils/transform.test.ts b/public/app/features/explore/TraceView/utils/transform.test.ts new file mode 100644 index 00000000000..dc92293493e --- /dev/null +++ b/public/app/features/explore/TraceView/utils/transform.test.ts @@ -0,0 +1,45 @@ +import { createDataFrame } from '@grafana/data'; + +import { transformTraceDataFrame } from './transform'; + +describe('transformTraceDataFrame()', () => { + const fields = [ + { name: 'traceID', values: ['trace1'] }, + { name: 'operationName', values: ['operation1'] }, + { name: 'kind', values: ['server'] }, + { name: 'tags', values: [[{ key: 'key1', value: 'value1' }]] }, + ]; + + it('should return transformed data', () => { + const dummyDataFrame = createDataFrame({ + fields: fields.concat([...fields, { name: 'spanID', values: ['span1'] }]), + }); + expect(transformTraceDataFrame(dummyDataFrame)).toEqual({ + processes: { span1: { serviceName: undefined, tags: undefined } }, + spans: [ + { + dataFrameRowIndex: 0, + duration: NaN, + flags: 0, + kind: 'server', + logs: [], + operationName: 'operation1', + processID: 'span1', + references: [], + spanID: 'span1', + startTime: NaN, + tags: [{ key: 'key1', value: 'value1' }], + traceID: 'trace1', + }, + ], + traceID: 'trace1', + }); + }); + + it('should return null for any span without a spanID', () => { + const dummyDataFrame = createDataFrame({ + fields: fields, + }); + expect(transformTraceDataFrame(dummyDataFrame)).toEqual(null); + }); +}); diff --git a/public/app/features/explore/TraceView/utils/transform.ts b/public/app/features/explore/TraceView/utils/transform.ts index 07f5d2a144e..f731ac653da 100644 --- a/public/app/features/explore/TraceView/utils/transform.ts +++ b/public/app/features/explore/TraceView/utils/transform.ts @@ -6,19 +6,26 @@ export function transformDataFrames(frame?: DataFrame): Trace | null { if (!frame) { return null; } - let data: TraceResponse = + let data: TraceResponse | null = frame.fields.length === 1 ? // For backward compatibility when we sent whole json response in a single field/value frame.fields[0].values[0] : transformTraceDataFrame(frame); + + if (!data) { + return null; + } return transformTraceData(data); } -function transformTraceDataFrame(frame: DataFrame): TraceResponse { +export function transformTraceDataFrame(frame: DataFrame): TraceResponse | null { const view = new DataFrameView(frame); const processes: Record = {}; for (let i = 0; i < view.length; i++) { const span = view.get(i); + if (!span.spanID) { + return null; + } if (!processes[span.spanID]) { processes[span.spanID] = { serviceName: span.serviceName, From 01add144b86ca42e848c6b15373fd70924bbc84b Mon Sep 17 00:00:00 2001 From: Santiago Date: Wed, 25 Oct 2023 11:52:48 +0200 Subject: [PATCH 047/180] Alerting: Send alerts to the remote Alertmanager (#77034) * Alerting: Rename remote.ExternalAlertmanager to remote.Alertmanager * Alerting: Send alerts to the remote Alertmanager * add ticker to readiness check, add tests * use options when creating a new sender.ExternaAlertmanager * unexport defaultMaxQueueCapacity * delete unused defaultConfig field * add debug log line when sending alerts to the remote alertmanager * move and refactor readiness check * update tests to not include defaultConfig --- pkg/services/ngalert/remote/alertmanager.go | 127 ++++++++++++------ .../ngalert/remote/alertmanager_test.go | 80 ++++------- pkg/services/ngalert/sender/router.go | 14 +- pkg/services/ngalert/sender/router_test.go | 6 +- pkg/services/ngalert/sender/sender.go | 46 ++++--- 5 files changed, 153 insertions(+), 120 deletions(-) diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 52ca4d6730d..073400a333d 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -7,14 +7,15 @@ import ( "net/http" "net/url" "strings" + "time" httptransport "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" - alertingNotify "github.com/grafana/alerting/notify" "github.com/grafana/grafana/pkg/infra/log" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier" + "github.com/grafana/grafana/pkg/services/ngalert/sender" amclient "github.com/prometheus/alertmanager/api/v2/client" amalert "github.com/prometheus/alertmanager/api/v2/client/alert" amalertgroup "github.com/prometheus/alertmanager/api/v2/client/alertgroup" @@ -22,21 +23,24 @@ import ( amsilence "github.com/prometheus/alertmanager/api/v2/client/silence" ) +const readyPath = "/-/ready" + type Alertmanager struct { - log log.Logger - url string - tenantID string - orgID int64 - amClient *amclient.AlertmanagerAPI - httpClient *http.Client - defaultConfig string + log log.Logger + orgID int64 + tenantID string + url string + + amClient *amclient.AlertmanagerAPI + httpClient *http.Client + ready bool + sender *sender.ExternalAlertmanager } type AlertmanagerConfig struct { URL string TenantID string BasicAuthPassword string - DefaultConfig string } func NewAlertmanager(cfg AlertmanagerConfig, orgID int64) (*Alertmanager, error) { @@ -56,28 +60,83 @@ func NewAlertmanager(cfg AlertmanagerConfig, orgID int64) (*Alertmanager, error) if err != nil { return nil, err } - u = u.JoinPath(amclient.DefaultBasePath) + u = u.JoinPath(amclient.DefaultBasePath) transport := httptransport.NewWithClient(u.Host, u.Path, []string{u.Scheme}, &client) - _, err = notifier.Load([]byte(cfg.DefaultConfig)) + // Using our client with custom headers and basic auth credentials. + doFunc := func(ctx context.Context, _ *http.Client, req *http.Request) (*http.Response, error) { + return client.Do(req.WithContext(ctx)) + } + s := sender.NewExternalAlertmanagerSender(sender.WithDoFunc(doFunc)) + s.Run() + + err = s.ApplyConfig(orgID, 0, []sender.ExternalAMcfg{{ + URL: cfg.URL, + }}) if err != nil { return nil, err } return &Alertmanager{ - amClient: amclient.New(transport, nil), - httpClient: &client, - log: log.New("ngalert.notifier.external-alertmanager"), - url: cfg.URL, - tenantID: cfg.TenantID, - orgID: orgID, - defaultConfig: cfg.DefaultConfig, + amClient: amclient.New(transport, nil), + httpClient: &client, + log: log.New("ngalert.remote.alertmanager"), + sender: s, + orgID: orgID, + tenantID: cfg.TenantID, + url: cfg.URL, }, nil } func (am *Alertmanager) ApplyConfig(ctx context.Context, config *models.AlertConfiguration) error { - return nil + if am.ready { + return nil + } + + return am.checkReadiness(ctx) +} + +func (am *Alertmanager) checkReadiness(ctx context.Context) error { + readyURL := strings.TrimSuffix(am.url, "/") + readyPath + req, err := http.NewRequestWithContext(ctx, http.MethodGet, readyURL, nil) + if err != nil { + return fmt.Errorf("error creating readiness request: %w", err) + } + + res, err := am.httpClient.Do(req) + if err != nil { + return fmt.Errorf("error performing readiness check: %w", err) + } + + defer func() { + if err := res.Body.Close(); err != nil { + am.log.Warn("Error closing response body", "err", err) + } + }() + + if res.StatusCode != http.StatusOK { + return fmt.Errorf("%w, status code: %d", notifier.ErrAlertmanagerNotReady, res.StatusCode) + } + + // Wait for active senders. + var attempts int + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + attempts++ + if len(am.sender.Alertmanagers()) > 0 { + am.log.Debug("Alertmanager readiness check successful", "attempts", attempts) + am.ready = true + return nil + } + case <-time.After(10 * time.Second): + return notifier.ErrAlertmanagerNotReady + } + } } func (am *Alertmanager) SaveAndApplyConfig(ctx context.Context, cfg *apimodels.PostableUserConfig) error { @@ -195,29 +254,10 @@ func (am *Alertmanager) GetAlertGroups(ctx context.Context, active, silenced, in return res.Payload, nil } -// TODO: implement PutAlerts in a way that is similar to what Prometheus does. -// This current implementation is only good for testing methods that retrieve alerts from the remote Alertmanager. -// More details in issue https://github.com/grafana/grafana/issues/76692 -func (am *Alertmanager) PutAlerts(ctx context.Context, postableAlerts apimodels.PostableAlerts) error { - defer func() { - if r := recover(); r != nil { - am.log.Error("Panic while putting alerts", "err", r) - } - }() - - alerts := make(alertingNotify.PostableAlerts, 0, len(postableAlerts.PostableAlerts)) - for _, pa := range postableAlerts.PostableAlerts { - alerts = append(alerts, &alertingNotify.PostableAlert{ - Annotations: pa.Annotations, - EndsAt: pa.EndsAt, - StartsAt: pa.StartsAt, - Alert: pa.Alert, - }) - } - - params := amalert.NewPostAlertsParamsWithContext(ctx).WithAlerts(alerts) - _, err := am.amClient.Alert.PostAlerts(params) - return err +func (am *Alertmanager) PutAlerts(ctx context.Context, alerts apimodels.PostableAlerts) error { + am.log.Debug("Sending alerts to a remote alertmanager", "url", am.url, "alerts", len(alerts.PostableAlerts)) + am.sender.SendAlerts(alerts) + return nil } func (am *Alertmanager) GetStatus() apimodels.GettableStatus { @@ -247,10 +287,11 @@ func (am *Alertmanager) TestTemplate(ctx context.Context, c apimodels.TestTempla } func (am *Alertmanager) StopAndWait() { + am.sender.Stop() } func (am *Alertmanager) Ready() bool { - return false + return am.ready } func (am *Alertmanager) FileStore() *notifier.FileStore { diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index 741d7a8ea76..822c2c6f05a 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -14,57 +14,32 @@ import ( "github.com/stretchr/testify/require" ) -const ( - validConfig = `{"template_files":{},"alertmanager_config":{"route":{"receiver":"grafana-default-email","group_by":["grafana_folder","alertname"]},"templates":null,"receivers":[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"","name":"some other name","type":"email","disableResolveMessage":false,"settings":{"addresses":"\u003cexample@email.com\u003e"},"secureSettings":null}]}]}}` - - // Valid config for Cloud AM, no `grafana_managed_receievers` field. - upstreamConfig = `{"template_files": {}, "alertmanager_config": "{\"global\": {\"smtp_from\": \"test@test.com\"}, \"route\": {\"receiver\": \"discord\"}, \"receivers\": [{\"name\": \"discord\", \"discord_configs\": [{\"webhook_url\": \"http://localhost:1234\"}]}]}"}` -) +// Valid config for Cloud AM, no `grafana_managed_receievers` field. +const upstreamConfig = `{"template_files": {}, "alertmanager_config": "{\"global\": {\"smtp_from\": \"test@test.com\"}, \"route\": {\"receiver\": \"discord\"}, \"receivers\": [{\"name\": \"discord\", \"discord_configs\": [{\"webhook_url\": \"http://localhost:1234\"}]}]}"}` func TestNewAlertmanager(t *testing.T) { tests := []struct { - name string - url string - tenantID string - password string - orgID int64 - defaultConfig string - expErr string + name string + url string + tenantID string + password string + orgID int64 + expErr string }{ { - name: "empty URL", - url: "", - tenantID: "1234", - password: "test", - defaultConfig: validConfig, - orgID: 1, - expErr: "empty URL for tenant 1234", + name: "empty URL", + url: "", + tenantID: "1234", + password: "test", + orgID: 1, + expErr: "empty URL for tenant 1234", }, { - name: "empty default config", - url: "http://localhost:8080", - tenantID: "1234", - defaultConfig: "", - password: "test", - orgID: 1, - expErr: "unable to parse Alertmanager configuration: unexpected end of JSON input", - }, - { - name: "invalid default config", - url: "http://localhost:8080", - tenantID: "1234", - defaultConfig: `{"invalid": true}`, - password: "test", - orgID: 1, - expErr: "unable to parse Alertmanager configuration: no route provided in config", - }, - { - name: "valid parameters", - url: "http://localhost:8080", - tenantID: "1234", - defaultConfig: validConfig, - password: "test", - orgID: 1, + name: "valid parameters", + url: "http://localhost:8080", + tenantID: "1234", + password: "test", + orgID: 1, }, } @@ -74,7 +49,6 @@ func TestNewAlertmanager(t *testing.T) { URL: test.url, TenantID: test.tenantID, BasicAuthPassword: test.password, - DefaultConfig: test.defaultConfig, } am, err := NewAlertmanager(cfg, test.orgID) if test.expErr != "" { @@ -85,7 +59,6 @@ func TestNewAlertmanager(t *testing.T) { require.NoError(tt, err) require.Equal(tt, am.tenantID, test.tenantID) require.Equal(tt, am.url, test.url) - require.Equal(tt, am.defaultConfig, test.defaultConfig) require.Equal(tt, am.OrgID(), test.orgID) require.NotNil(tt, am.amClient) require.NotNil(tt, am.httpClient) @@ -109,7 +82,6 @@ func TestIntegrationRemoteAlertmanagerSilences(t *testing.T) { URL: amURL + "/alertmanager", TenantID: tenantID, BasicAuthPassword: password, - DefaultConfig: validConfig, } am, err := NewAlertmanager(cfg, 1) require.NoError(t, err) @@ -189,11 +161,14 @@ func TestIntegrationRemoteAlertmanagerAlerts(t *testing.T) { URL: amURL + "/alertmanager", TenantID: tenantID, BasicAuthPassword: password, - DefaultConfig: validConfig, } am, err := NewAlertmanager(cfg, 1) require.NoError(t, err) + // Wait until the Alertmanager is ready to send alerts. + require.NoError(t, am.checkReadiness(context.Background())) + require.True(t, am.Ready()) + // We should have no alerts and no groups at first. alerts, err := am.GetAlerts(context.Background(), true, true, true, []string{}, "") require.NoError(t, err) @@ -214,9 +189,11 @@ func TestIntegrationRemoteAlertmanagerAlerts(t *testing.T) { require.NoError(t, err) // We should have two alerts and one group now. - alerts, err = am.GetAlerts(context.Background(), true, true, true, []string{}, "") - require.NoError(t, err) - require.Equal(t, 2, len(alerts)) + require.Eventually(t, func() bool { + alerts, err = am.GetAlerts(context.Background(), true, true, true, []string{}, "") + require.NoError(t, err) + return len(alerts) == 2 + }, 16*time.Second, 1*time.Second) alertGroups, err = am.GetAlertGroups(context.Background(), true, true, true, []string{}, "") require.NoError(t, err) @@ -245,7 +222,6 @@ func TestIntegrationRemoteAlertmanagerReceivers(t *testing.T) { URL: amURL + "/alertmanager", TenantID: tenantID, BasicAuthPassword: password, - DefaultConfig: validConfig, } am, err := NewAlertmanager(cfg, 1) diff --git a/pkg/services/ngalert/sender/router.go b/pkg/services/ngalert/sender/router.go index cce4af1399b..6cac839f3cb 100644 --- a/pkg/services/ngalert/sender/router.go +++ b/pkg/services/ngalert/sender/router.go @@ -188,10 +188,10 @@ func (d *AlertsRouter) SyncAndApplyConfigFromDatabase() error { return nil } -func buildRedactedAMs(l log.Logger, alertmanagers []externalAMcfg, ordId int64) []string { +func buildRedactedAMs(l log.Logger, alertmanagers []ExternalAMcfg, ordId int64) []string { var redactedAMs []string for _, am := range alertmanagers { - parsedAM, err := url.Parse(am.amURL) + parsedAM, err := url.Parse(am.URL) if err != nil { l.Error("Failed to parse alertmanager string", "org", ordId, "error", err) continue @@ -208,9 +208,9 @@ func asSHA256(strings []string) string { return fmt.Sprintf("%x", h.Sum(nil)) } -func (d *AlertsRouter) alertmanagersFromDatasources(orgID int64) ([]externalAMcfg, error) { +func (d *AlertsRouter) alertmanagersFromDatasources(orgID int64) ([]ExternalAMcfg, error) { var ( - alertmanagers []externalAMcfg + alertmanagers []ExternalAMcfg ) // We might have alertmanager datasources that are acting as external // alertmanager, let's fetch them. @@ -246,9 +246,9 @@ func (d *AlertsRouter) alertmanagersFromDatasources(orgID int64) ([]externalAMcf "error", err) continue } - alertmanagers = append(alertmanagers, externalAMcfg{ - amURL: amURL, - headers: headers, + alertmanagers = append(alertmanagers, ExternalAMcfg{ + URL: amURL, + Headers: headers, }) } return alertmanagers, nil diff --git a/pkg/services/ngalert/sender/router_test.go b/pkg/services/ngalert/sender/router_test.go index 5fb627ca09d..e9cfafeed8f 100644 --- a/pkg/services/ngalert/sender/router_test.go +++ b/pkg/services/ngalert/sender/router_test.go @@ -590,10 +590,10 @@ func TestAlertManagers_buildRedactedAMs(t *testing.T) { for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { - var cfgs []externalAMcfg + var cfgs []ExternalAMcfg for _, url := range tt.amUrls { - cfgs = append(cfgs, externalAMcfg{ - amURL: url, + cfgs = append(cfgs, ExternalAMcfg{ + URL: url, }) } require.Equal(t, tt.expected, buildRedactedAMs(&fakeLogger, cfgs, tt.orgId)) diff --git a/pkg/services/ngalert/sender/sender.go b/pkg/services/ngalert/sender/sender.go index 8e09015a463..eef29402c05 100644 --- a/pkg/services/ngalert/sender/sender.go +++ b/pkg/services/ngalert/sender/sender.go @@ -5,6 +5,7 @@ import ( "crypto/md5" "errors" "fmt" + "net/http" "net/url" "sort" "strings" @@ -40,35 +41,46 @@ type ExternalAlertmanager struct { sdManager *discovery.Manager } -type externalAMcfg struct { - amURL string - headers map[string]string +type ExternalAMcfg struct { + URL string + Headers map[string]string } -func (cfg *externalAMcfg) SHA256() string { - return asSHA256([]string{cfg.headerString(), cfg.amURL}) +type Option func(*ExternalAlertmanager) + +type doFunc func(context.Context, *http.Client, *http.Request) (*http.Response, error) + +// WithDoFunc receives a function to use when making HTTP requests from the Manager. +func WithDoFunc(doFunc doFunc) Option { + return func(s *ExternalAlertmanager) { + s.manager.opts.Do = doFunc + } +} + +func (cfg *ExternalAMcfg) SHA256() string { + return asSHA256([]string{cfg.headerString(), cfg.URL}) } // headersString transforms all the headers in a sorted way as a // single string so it can be used for hashing and comparing. -func (cfg *externalAMcfg) headerString() string { +func (cfg *ExternalAMcfg) headerString() string { var result strings.Builder - headerKeys := make([]string, 0, len(cfg.headers)) - for key := range cfg.headers { + headerKeys := make([]string, 0, len(cfg.Headers)) + for key := range cfg.Headers { headerKeys = append(headerKeys, key) } sort.Strings(headerKeys) for _, key := range headerKeys { - result.WriteString(fmt.Sprintf("%s:%s", key, cfg.headers[key])) + result.WriteString(fmt.Sprintf("%s:%s", key, cfg.Headers[key])) } return result.String() } -func NewExternalAlertmanagerSender() *ExternalAlertmanager { +func NewExternalAlertmanagerSender(opts ...Option) *ExternalAlertmanager { l := log.New("ngalert.sender.external-alertmanager") sdCtx, sdCancel := context.WithCancel(context.Background()) s := &ExternalAlertmanager{ @@ -85,11 +97,15 @@ func NewExternalAlertmanagerSender() *ExternalAlertmanager { s.sdManager = discovery.NewManager(sdCtx, s.logger) + for _, opt := range opts { + opt(s) + } + return s } // ApplyConfig syncs a configuration with the sender. -func (s *ExternalAlertmanager) ApplyConfig(orgId, id int64, alertmanagers []externalAMcfg) error { +func (s *ExternalAlertmanager) ApplyConfig(orgId, id int64, alertmanagers []ExternalAMcfg) error { notifierCfg, headers, err := buildNotifierConfig(alertmanagers) if err != nil { return err @@ -160,11 +176,11 @@ func (s *ExternalAlertmanager) DroppedAlertmanagers() []*url.URL { return s.manager.DroppedAlertmanagers() } -func buildNotifierConfig(alertmanagers []externalAMcfg) (*config.Config, map[string]map[string]string, error) { +func buildNotifierConfig(alertmanagers []ExternalAMcfg) (*config.Config, map[string]map[string]string, error) { amConfigs := make([]*config.AlertmanagerConfig, 0, len(alertmanagers)) headers := map[string]map[string]string{} for i, am := range alertmanagers { - u, err := url.Parse(am.amURL) + u, err := url.Parse(am.URL) if err != nil { return nil, nil, err } @@ -185,10 +201,10 @@ func buildNotifierConfig(alertmanagers []externalAMcfg) (*config.Config, map[str ServiceDiscoveryConfigs: sdConfig, } - if am.headers != nil { + if am.Headers != nil { // The key has the same format as the AlertmanagerConfigs.ToMap() would generate // so we can use it later on when working with the alertmanager config map. - headers[fmt.Sprintf("config-%d", i)] = am.headers + headers[fmt.Sprintf("config-%d", i)] = am.Headers } // Check the URL for basic authentication information first From 00c9981c51cccc17a11bedbb2a6da76d837019ca Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Wed, 25 Oct 2023 13:20:36 +0300 Subject: [PATCH 048/180] Chore: Fix missing enterprise operation from swagger (#77122) --- public/api-enterprise-spec.json | 25 +++++++++++++++++++++++++ public/api-merged.json | 3 ++- public/openapi3.json | 3 ++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index 9701d0b0a76..f9e390481df 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -639,6 +639,31 @@ } } }, + "/admin/ldap-sync-status": { + "get": { + "description": "You need to have a permission with action `ldap.status:read`.", + "tags": [ + "ldap_debug", + "enterprise" + ], + "summary": "Returns the current state of the LDAP background sync integration.", + "operationId": "getSyncStatus", + "responses": { + "200": { + "$ref": "#/responses/getSyncStatusResponse" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "403": { + "$ref": "#/responses/forbiddenError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, "/admin/provisioning/access-control/reload": { "post": { "tags": [ diff --git a/public/api-merged.json b/public/api-merged.json index 2292da639b4..8194ddb54cc 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -643,7 +643,8 @@ "get": { "description": "You need to have a permission with action `ldap.status:read`.", "tags": [ - "ldap_debug" + "ldap_debug", + "enterprise" ], "summary": "Returns the current state of the LDAP background sync integration.", "operationId": "getSyncStatus", diff --git a/public/openapi3.json b/public/openapi3.json index 61f3dc35912..c855b84e7d4 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -12510,7 +12510,8 @@ }, "summary": "Returns the current state of the LDAP background sync integration.", "tags": [ - "ldap_debug" + "ldap_debug", + "enterprise" ] } }, From b156267e394384ddc890435951d6f7fd380980ef Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Wed, 25 Oct 2023 12:21:01 +0200 Subject: [PATCH 049/180] ServiceAccount: Add pagination to service accout table (#77044) * Add pagination to service account table --- .../ServiceAccountsListPage.test.tsx | 2 ++ .../ServiceAccountsListPage.tsx | 20 ++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx index 8ccb8387f81..bce6b83e0d0 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx @@ -16,6 +16,7 @@ jest.mock('app/core/core', () => ({ })); const setup = (propOverrides: Partial) => { + const changePageMock = jest.fn(); const changeQueryMock = jest.fn(); const fetchACOptionsMock = jest.fn(); const fetchServiceAccountsMock = jest.fn(); @@ -33,6 +34,7 @@ const setup = (propOverrides: Partial) => { showPaging: false, totalPages: 1, serviceAccounts: [], + changePage: changePageMock, changeQuery: changeQueryMock, fetchACOptions: fetchACOptionsMock, fetchServiceAccounts: fetchServiceAccountsMock, diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx index a5b1c7cdbd0..8aedc6dc563 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx @@ -4,7 +4,16 @@ import React, { useEffect, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { GrafanaTheme2, OrgRole } from '@grafana/data'; -import { ConfirmModal, FilterInput, LinkButton, RadioButtonGroup, useStyles2, InlineField } from '@grafana/ui'; +import { + ConfirmModal, + FilterInput, + LinkButton, + RadioButtonGroup, + useStyles2, + InlineField, + Pagination, +} from '@grafana/ui'; +import { Flex } from '@grafana/ui/src/unstable'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import { Page } from 'app/core/components/Page/Page'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; @@ -15,6 +24,7 @@ import { CreateTokenModal, ServiceAccountToken } from './components/CreateTokenM import ServiceAccountListItem from './components/ServiceAccountsListItem'; import { changeQuery, + changePage, fetchACOptions, fetchServiceAccounts, deleteServiceAccount, @@ -34,6 +44,7 @@ function mapStateToProps(state: StoreState) { } const mapDispatchToProps = { + changePage, changeQuery, fetchACOptions, fetchServiceAccounts, @@ -46,6 +57,9 @@ const mapDispatchToProps = { const connector = connect(mapStateToProps, mapDispatchToProps); export const ServiceAccountsListPageUnconnected = ({ + page, + changePage, + totalPages, serviceAccounts, isLoading, roleOptions, @@ -238,6 +252,10 @@ export const ServiceAccountsListPageUnconnected = ({ ))} + + + +
)} From c25ea17d10033fa54dac4d4571cc0b9503c619b7 Mon Sep 17 00:00:00 2001 From: Gareth Dawson Date: Wed, 25 Oct 2023 11:38:16 +0100 Subject: [PATCH 050/180] SQL: Fix config page backwards compatibility (#76951) fix --- .../sql/components/configuration/Divider.tsx | 21 +++++++++++++++++++ .../configuration/ConfigurationEditor.tsx | 2 +- .../configuration/ConfigurationEditor.tsx | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 public/app/features/plugins/sql/components/configuration/Divider.tsx diff --git a/public/app/features/plugins/sql/components/configuration/Divider.tsx b/public/app/features/plugins/sql/components/configuration/Divider.tsx new file mode 100644 index 00000000000..c6582eb725f --- /dev/null +++ b/public/app/features/plugins/sql/components/configuration/Divider.tsx @@ -0,0 +1,21 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { useStyles2 } from '@grafana/ui'; + +// this custom component is necessary because the Grafana UI component is not backwards compatible with Grafana < 10.1.0 +export const Divider = () => { + const styles = useStyles2(getStyles); + return
; +}; + +const getStyles = (theme: GrafanaTheme2) => { + return { + horizontalDivider: css({ + borderTop: `1px solid ${theme.colors.border.weak}`, + margin: theme.spacing(2, 0), + width: '100%', + }), + }; +}; diff --git a/public/app/plugins/datasource/mysql/configuration/ConfigurationEditor.tsx b/public/app/plugins/datasource/mysql/configuration/ConfigurationEditor.tsx index 7f40650211a..055dc88e276 100644 --- a/public/app/plugins/datasource/mysql/configuration/ConfigurationEditor.tsx +++ b/public/app/plugins/datasource/mysql/configuration/ConfigurationEditor.tsx @@ -11,7 +11,6 @@ import { ConfigSection, ConfigSubSection, DataSourceDescription, Stack } from '@ import { config } from '@grafana/runtime'; import { Collapse, - Divider, Field, Icon, Input, @@ -22,6 +21,7 @@ import { Tooltip, } from '@grafana/ui'; import { ConnectionLimits } from 'app/features/plugins/sql/components/configuration/ConnectionLimits'; +import { Divider } from 'app/features/plugins/sql/components/configuration/Divider'; import { TLSSecretsConfig } from 'app/features/plugins/sql/components/configuration/TLSSecretsConfig'; import { useMigrateDatabaseFields } from 'app/features/plugins/sql/components/configuration/useMigrateDatabaseFields'; diff --git a/public/app/plugins/datasource/postgres/configuration/ConfigurationEditor.tsx b/public/app/plugins/datasource/postgres/configuration/ConfigurationEditor.tsx index 559d0b63338..3c1ea733a2f 100644 --- a/public/app/plugins/datasource/postgres/configuration/ConfigurationEditor.tsx +++ b/public/app/plugins/datasource/postgres/configuration/ConfigurationEditor.tsx @@ -11,7 +11,6 @@ import { import { ConfigSection, ConfigSubSection, DataSourceDescription, Stack } from '@grafana/experimental'; import { config } from '@grafana/runtime'; import { - Divider, Input, Select, SecretInput, @@ -24,6 +23,7 @@ import { Collapse, } from '@grafana/ui'; import { ConnectionLimits } from 'app/features/plugins/sql/components/configuration/ConnectionLimits'; +import { Divider } from 'app/features/plugins/sql/components/configuration/Divider'; import { TLSSecretsConfig } from 'app/features/plugins/sql/components/configuration/TLSSecretsConfig'; import { useMigrateDatabaseFields } from 'app/features/plugins/sql/components/configuration/useMigrateDatabaseFields'; From 1bc81b7bd15a6c465a987820e4ad3f1a52423114 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Wed, 25 Oct 2023 12:40:30 +0200 Subject: [PATCH 051/180] auth: migrate api interface implementation (#77040) * expand serviceaccount service interface * implemet FakeServiceAccountService * Replace SA service interface from api * merge sa proxy tests with new fake service * implement DeleteServiceAccountToken * add test for DeleteServiceAccountToken --- pkg/services/serviceaccounts/api/api.go | 22 +-- pkg/services/serviceaccounts/api/api_test.go | 58 +------- .../serviceaccounts/api/token_test.go | 7 +- .../serviceaccounts/extsvcaccounts/models.go | 11 +- pkg/services/serviceaccounts/proxy/service.go | 29 ++++ .../serviceaccounts/proxy/service_test.go | 127 ++++++++---------- .../serviceaccounts/serviceaccounts.go | 14 +- pkg/services/serviceaccounts/tests/fakes.go | 69 ++++++++++ pkg/services/serviceaccounts/tests/mocks.go | 35 ++++- 9 files changed, 214 insertions(+), 158 deletions(-) create mode 100644 pkg/services/serviceaccounts/tests/fakes.go diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index 641e8233225..bf9d8ed3b54 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -1,7 +1,6 @@ package api import ( - "context" "net/http" "strconv" @@ -11,7 +10,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/middleware/requestmeta" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/auth/identity" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" @@ -23,7 +21,7 @@ import ( type ServiceAccountsAPI struct { cfg *setting.Cfg - service service + service serviceaccounts.Service accesscontrol accesscontrol.AccessControl accesscontrolService accesscontrol.Service RouterRegister routing.RouteRegister @@ -31,25 +29,9 @@ type ServiceAccountsAPI struct { permissionService accesscontrol.ServiceAccountPermissionsService } -// Service implements the API exposed methods for service accounts. -type service interface { - CreateServiceAccount(ctx context.Context, orgID int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) - RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) - UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, - saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) - SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) - ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error) - DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error - MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error) - MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error - // Service account tokens - AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *serviceaccounts.AddServiceAccountTokenCommand) (*apikey.APIKey, error) - DeleteServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error -} - func NewServiceAccountsAPI( cfg *setting.Cfg, - service service, + service serviceaccounts.Service, accesscontrol accesscontrol.AccessControl, accesscontrolService accesscontrol.Service, routerRegister routing.RouteRegister, diff --git a/pkg/services/serviceaccounts/api/api_test.go b/pkg/services/serviceaccounts/api/api_test.go index 636acfc0ba6..01e5fa1c3b6 100644 --- a/pkg/services/serviceaccounts/api/api_test.go +++ b/pkg/services/serviceaccounts/api/api_test.go @@ -1,7 +1,6 @@ package api import ( - "context" "encoding/json" "fmt" "net/http" @@ -16,9 +15,9 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" - "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/serviceaccounts" + satests "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web/webtest" @@ -82,7 +81,7 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { server := setupTests(t, func(a *ServiceAccountsAPI) { - a.service = &fakeServiceAccountService{ExpectedServiceAccount: tt.expectedSA, ExpectedErr: tt.expectedErr} + a.service = &satests.FakeServiceAccountService{ExpectedServiceAccount: tt.expectedSA, ExpectedErr: tt.expectedErr} }) req := server.NewRequest(http.MethodPost, "/api/serviceaccounts/", strings.NewReader(tt.body)) webtest.RequestWithSignedInUser(req, &user.SignedInUser{ @@ -162,7 +161,7 @@ func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { server := setupTests(t, func(a *ServiceAccountsAPI) { - a.service = &fakeServiceAccountService{ExpectedServiceAccountProfile: tt.expectedSA} + a.service = &satests.FakeServiceAccountService{ExpectedServiceAccountProfile: tt.expectedSA} }) req := server.NewGetRequest(fmt.Sprintf("/api/serviceaccounts/%d", tt.id)) webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}) @@ -224,7 +223,7 @@ func TestServiceAccountsAPI_UpdateServiceAccount(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { server := setupTests(t, func(a *ServiceAccountsAPI) { - a.service = &fakeServiceAccountService{ExpectedServiceAccountProfile: tt.expectedSA} + a.service = &satests.FakeServiceAccountService{ExpectedServiceAccountProfile: tt.expectedSA} }) req := server.NewRequest(http.MethodPatch, fmt.Sprintf("/api/serviceaccounts/%d", tt.id), strings.NewReader(tt.body)) @@ -278,7 +277,7 @@ func TestServiceAccountsAPI_MigrateApiKeysToServiceAccounts(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { server := setupTests(t, func(a *ServiceAccountsAPI) { - a.service = &fakeServiceAccountService{ExpectedMigrationResult: tt.expectedMigrationResult} + a.service = &satests.FakeServiceAccountService{ExpectedMigrationResult: tt.expectedMigrationResult} }) req := server.NewRequest(http.MethodPost, "/api/serviceaccounts/migrate", nil) @@ -303,7 +302,7 @@ func setupTests(t *testing.T, opts ...func(a *ServiceAccountsAPI)) *webtest.Serv cfg := setting.NewCfg() api := &ServiceAccountsAPI{ cfg: cfg, - service: &fakeServiceAccountService{}, + service: &satests.FakeServiceAccountService{}, accesscontrolService: &actest.FakeService{}, accesscontrol: acimpl.ProvideAccessControl(cfg), RouterRegister: routing.NewRouteRegister(), @@ -317,48 +316,3 @@ func setupTests(t *testing.T, opts ...func(a *ServiceAccountsAPI)) *webtest.Serv api.RegisterAPIEndpoints() return webtest.NewServer(t, api.RouterRegister) } - -var _ service = new(fakeServiceAccountService) - -type fakeServiceAccountService struct { - service - ExpectedErr error - ExpectedAPIKey *apikey.APIKey - ExpectedServiceAccountTokens []apikey.APIKey - ExpectedServiceAccount *serviceaccounts.ServiceAccountDTO - ExpectedServiceAccountProfile *serviceaccounts.ServiceAccountProfileDTO - ExpectedMigrationResult *serviceaccounts.MigrationResult -} - -func (f *fakeServiceAccountService) CreateServiceAccount(ctx context.Context, orgID int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { - return f.ExpectedServiceAccount, f.ExpectedErr -} - -func (f *fakeServiceAccountService) DeleteServiceAccount(ctx context.Context, orgID, id int64) error { - return f.ExpectedErr -} - -func (f *fakeServiceAccountService) RetrieveServiceAccount(ctx context.Context, orgID, id int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { - return f.ExpectedServiceAccountProfile, f.ExpectedErr -} - -func (f *fakeServiceAccountService) ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error) { - return f.ExpectedServiceAccountTokens, f.ExpectedErr -} - -func (f *fakeServiceAccountService) UpdateServiceAccount(ctx context.Context, orgID, id int64, cmd *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) { - return f.ExpectedServiceAccountProfile, f.ExpectedErr -} - -func (f *fakeServiceAccountService) AddServiceAccountToken(ctx context.Context, id int64, cmd *serviceaccounts.AddServiceAccountTokenCommand) (*apikey.APIKey, error) { - return f.ExpectedAPIKey, f.ExpectedErr -} - -func (f *fakeServiceAccountService) DeleteServiceAccountToken(ctx context.Context, orgID, id, tokenID int64) error { - return f.ExpectedErr -} - -func (f *fakeServiceAccountService) MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error) { - fmt.Printf("fake migration result: %v", f.ExpectedMigrationResult) - return f.ExpectedMigrationResult, f.ExpectedErr -} diff --git a/pkg/services/serviceaccounts/api/token_test.go b/pkg/services/serviceaccounts/api/token_test.go index 9796e58d254..5c672c16d86 100644 --- a/pkg/services/serviceaccounts/api/token_test.go +++ b/pkg/services/serviceaccounts/api/token_test.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/serviceaccounts" + satests "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web/webtest" ) @@ -43,7 +44,7 @@ func TestServiceAccountsAPI_ListTokens(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { server := setupTests(t, func(a *ServiceAccountsAPI) { - a.service = &fakeServiceAccountService{} + a.service = &satests.FakeServiceAccountService{} }) req := server.NewGetRequest(fmt.Sprintf("/api/serviceaccounts/%d/tokens", tt.id)) webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}) @@ -109,7 +110,7 @@ func TestServiceAccountsAPI_CreateToken(t *testing.T) { t.Run(tt.desc, func(t *testing.T) { server := setupTests(t, func(a *ServiceAccountsAPI) { a.cfg.ApiKeyMaxSecondsToLive = tt.tokenTTL - a.service = &fakeServiceAccountService{ + a.service = &satests.FakeServiceAccountService{ ExpectedErr: tt.expectedErr, ExpectedAPIKey: tt.expectedAPIKey, } @@ -163,7 +164,7 @@ func TestServiceAccountsAPI_DeleteToken(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { server := setupTests(t, func(a *ServiceAccountsAPI) { - a.service = &fakeServiceAccountService{ExpectedErr: tt.expectedErr} + a.service = &satests.FakeServiceAccountService{ExpectedErr: tt.expectedErr} }) req := server.NewRequest(http.MethodDelete, fmt.Sprintf("/api/serviceaccounts/%d/tokens/%d", tt.saID, tt.apikeyID), nil) diff --git a/pkg/services/serviceaccounts/extsvcaccounts/models.go b/pkg/services/serviceaccounts/extsvcaccounts/models.go index 3485d0289a4..94154cddc0d 100644 --- a/pkg/services/serviceaccounts/extsvcaccounts/models.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/models.go @@ -15,12 +15,13 @@ const ( ) var ( - ErrCannotBeDeleted = errutil.BadRequest("extsvcaccounts.ErrCannotBeDeleted", errutil.WithPublicMessage("external service account cannot be deleted")) - ErrInvalidName = errutil.BadRequest("extsvcaccounts.ErrInvalidName", errutil.WithPublicMessage("only external service account names can be prefixed with 'extsvc-'")) - ErrCannotBeUpdated = errutil.BadRequest("extsvcaccounts.ErrCannotBeUpdated", errutil.WithPublicMessage("external service account cannot be updated")) - ErrCannotCreateToken = errutil.BadRequest("extsvcaccounts.ErrCannotCreateToken", errutil.WithPublicMessage("cannot add external service account token")) - + ErrCannotBeDeleted = errutil.BadRequest("extsvcaccounts.ErrCannotBeDeleted", errutil.WithPublicMessage("external service account cannot be deleted")) + ErrCannotBeUpdated = errutil.BadRequest("extsvcaccounts.ErrCannotBeUpdated", errutil.WithPublicMessage("external service account cannot be updated")) + ErrCannotCreateToken = errutil.BadRequest("extsvcaccounts.ErrCannotCreateToken", errutil.WithPublicMessage("cannot add external service account token")) + ErrCannotDeleteToken = errutil.BadRequest("extsvcaccounts.ErrCannotDeleteToken", errutil.WithPublicMessage("cannot delete external service account token")) + ErrCannotListTokens = errutil.BadRequest("extsvcaccounts.ErrCannotListTokens", errutil.WithPublicMessage("cannot list external service account tokens")) ErrCredentialsNotFound = errutil.NotFound("extsvcaccounts.credentials-not-found") + ErrInvalidName = errutil.BadRequest("extsvcaccounts.ErrInvalidName", errutil.WithPublicMessage("only external service account names can be prefixed with 'extsvc-'")) ) // Credentials represents the credentials associated to an external service diff --git a/pkg/services/serviceaccounts/proxy/service.go b/pkg/services/serviceaccounts/proxy/service.go index d6bda2a2452..6d77ec33692 100644 --- a/pkg/services/serviceaccounts/proxy/service.go +++ b/pkg/services/serviceaccounts/proxy/service.go @@ -2,6 +2,7 @@ package proxy import ( "context" + "fmt" "strings" "github.com/grafana/grafana/pkg/infra/log" @@ -68,6 +69,34 @@ func (s *ServiceAccountsProxy) DeleteServiceAccount(ctx context.Context, orgID, return s.proxiedService.DeleteServiceAccount(ctx, orgID, serviceAccountID) } +func (s *ServiceAccountsProxy) DeleteServiceAccountToken(ctx context.Context, orgID int64, serviceAccountID int64, tokenID int64) error { + sa, err := s.proxiedService.RetrieveServiceAccount(ctx, 0, serviceAccountID) + if err != nil { + return err + } + + fmt.Println("sa.Login: ", sa.Login) + + if isExternalServiceAccount(sa.Login) { + s.log.Error("unable to delete tokens for external service accounts", "serviceAccountID", serviceAccountID) + return extsvcaccounts.ErrCannotDeleteToken + } + + return s.proxiedService.DeleteServiceAccountToken(ctx, sa.OrgId, serviceAccountID, tokenID) +} + +func (s *ServiceAccountsProxy) ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error) { + return s.proxiedService.ListTokens(ctx, query) +} + +func (s *ServiceAccountsProxy) MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error { + return s.proxiedService.MigrateApiKey(ctx, orgID, keyId) +} + +func (s *ServiceAccountsProxy) MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error) { + return s.proxiedService.MigrateApiKeysToServiceAccounts(ctx, orgID) +} + func (s *ServiceAccountsProxy) RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { sa, err := s.proxiedService.RetrieveServiceAccount(ctx, orgID, serviceAccountID) if err != nil { diff --git a/pkg/services/serviceaccounts/proxy/service_test.go b/pkg/services/serviceaccounts/proxy/service_test.go index e9bc7461618..1701d9fbc0d 100644 --- a/pkg/services/serviceaccounts/proxy/service_test.go +++ b/pkg/services/serviceaccounts/proxy/service_test.go @@ -8,56 +8,18 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/extsvcaccounts" + "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" ) -type FakeServiceAccountsService struct { - ExpectedServiceAccountProfileDTO *serviceaccounts.ServiceAccountProfileDTO - ExpectedSearchOrgServiceAccountsResult *serviceaccounts.SearchOrgServiceAccountsResult -} - -var _ serviceaccounts.Service = (*FakeServiceAccountsService)(nil) - -func newServiceAccountServiceFake() *FakeServiceAccountsService { - return &FakeServiceAccountsService{} -} - -func (f *FakeServiceAccountsService) CreateServiceAccount(ctx context.Context, orgID int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { - return nil, nil -} - -func (f *FakeServiceAccountsService) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error { - return nil -} - -func (f *FakeServiceAccountsService) RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { - return f.ExpectedServiceAccountProfileDTO, nil -} - -func (f *FakeServiceAccountsService) RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) { - return 0, nil -} - -func (f *FakeServiceAccountsService) UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, - saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) { - return nil, nil -} - -func (f *FakeServiceAccountsService) AddServiceAccountToken(ctx context.Context, serviceAccountID int64, - cmd *serviceaccounts.AddServiceAccountTokenCommand) (*apikey.APIKey, error) { - return nil, nil -} - -func (f *FakeServiceAccountsService) SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) { - return f.ExpectedSearchOrgServiceAccountsResult, nil -} +var _ serviceaccounts.Service = (*tests.FakeServiceAccountService)(nil) func TestProvideServiceAccount_crudServiceAccount(t *testing.T) { testOrgId := int64(1) testServiceAccountId := int64(1) - serviceMock := newServiceAccountServiceFake() + testServiceAccountTokenId := int64(1) + serviceMock := &tests.FakeServiceAccountService{} svc := ServiceAccountsProxy{ log.New("test"), serviceMock, @@ -118,13 +80,44 @@ func TestProvideServiceAccount_crudServiceAccount(t *testing.T) { for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { - serviceMock.ExpectedServiceAccountProfileDTO = tc.expectedServiceAccount + serviceMock.ExpectedServiceAccountProfile = tc.expectedServiceAccount err := svc.DeleteServiceAccount(context.Background(), testOrgId, testServiceAccountId) assert.Equal(t, err, tc.expectedError, tc.description) }) } }) + t.Run("should delete service account", func(t *testing.T) { + testCases := []struct { + description string + expectedError error + expectedServiceAccount *serviceaccounts.ServiceAccountProfileDTO + }{ + { + description: "should allow to delete a service account token", + expectedError: nil, + expectedServiceAccount: &serviceaccounts.ServiceAccountProfileDTO{ + Login: "my-service-account", + }, + }, + { + description: "should not allow to delete a external service account token", + expectedError: extsvcaccounts.ErrCannotDeleteToken, + expectedServiceAccount: &serviceaccounts.ServiceAccountProfileDTO{ + Login: "sa-extsvc-my-service-account", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + serviceMock.ExpectedServiceAccountProfile = tc.expectedServiceAccount + err := svc.DeleteServiceAccountToken(context.Background(), testOrgId, testServiceAccountId, testServiceAccountTokenId) + assert.Equal(t, err, tc.expectedError, tc.description) + }) + } + }) + t.Run("should retrieve service account with IsExternal field", func(t *testing.T) { testCases := []struct { description string @@ -149,7 +142,7 @@ func TestProvideServiceAccount_crudServiceAccount(t *testing.T) { for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { - serviceMock.ExpectedServiceAccountProfileDTO = tc.expectedServiceAccount + serviceMock.ExpectedServiceAccountProfile = tc.expectedServiceAccount sa, err := svc.RetrieveServiceAccount(context.Background(), testOrgId, testServiceAccountId) assert.NoError(t, err, tc.description) assert.Equal(t, tc.expectedIsExternal, sa.IsExternal, tc.description) @@ -157,6 +150,23 @@ func TestProvideServiceAccount_crudServiceAccount(t *testing.T) { } }) + t.Run("should mark external service accounts correctly", func(t *testing.T) { + serviceMock.ExpectedSearchOrgServiceAccountsResult = &serviceaccounts.SearchOrgServiceAccountsResult{ + TotalCount: 2, + ServiceAccounts: []*serviceaccounts.ServiceAccountDTO{ + {Login: "test"}, + {Login: serviceaccounts.ServiceAccountPrefix + serviceaccounts.ExtSvcPrefix + "test"}, + }, + Page: 1, + PerPage: 2, + } + res, err := svc.SearchOrgServiceAccounts(context.Background(), &serviceaccounts.SearchOrgServiceAccountsQuery{OrgID: 1}) + require.Len(t, res.ServiceAccounts, 2) + require.NoError(t, err) + require.False(t, res.ServiceAccounts[0].IsExternal) + require.True(t, res.ServiceAccounts[1].IsExternal) + }) + t.Run("should update service account", func(t *testing.T) { nameWithoutProtectedPrefix := "my-updated-service-account" nameWithProtectedPrefix := "extsvc-my-updated-service-account" @@ -211,7 +221,7 @@ func TestProvideServiceAccount_crudServiceAccount(t *testing.T) { for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { tc := tc - serviceMock.ExpectedServiceAccountProfileDTO = tc.expectedServiceAccount + serviceMock.ExpectedServiceAccountProfile = tc.expectedServiceAccount _, err := svc.UpdateServiceAccount(context.Background(), testOrgId, testServiceAccountId, &tc.form) assert.Equal(t, tc.expectedError, err, tc.description) }) @@ -250,7 +260,7 @@ func TestProvideServiceAccount_crudServiceAccount(t *testing.T) { for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { tc := tc - serviceMock.ExpectedServiceAccountProfileDTO = tc.expectedServiceAccount + serviceMock.ExpectedServiceAccountProfile = tc.expectedServiceAccount _, err := svc.AddServiceAccountToken(context.Background(), testServiceAccountId, &tc.cmd) assert.Equal(t, tc.expectedError, err, tc.description) }) @@ -264,28 +274,3 @@ func TestProvideServiceAccount_crudServiceAccount(t *testing.T) { assert.True(t, isExternalServiceAccount("sa-extsvc-my-service-account")) }) } - -func TestProvideServiceAccount_SearchServiceAccount(t *testing.T) { - serviceMock := newServiceAccountServiceFake() - svc := ServiceAccountsProxy{ - log.New("test"), - serviceMock, - } - - t.Run("should mark external service accounts correctly", func(t *testing.T) { - serviceMock.ExpectedSearchOrgServiceAccountsResult = &serviceaccounts.SearchOrgServiceAccountsResult{ - TotalCount: 2, - ServiceAccounts: []*serviceaccounts.ServiceAccountDTO{ - {Login: "test"}, - {Login: serviceaccounts.ServiceAccountPrefix + serviceaccounts.ExtSvcPrefix + "test"}, - }, - Page: 1, - PerPage: 2, - } - res, err := svc.SearchOrgServiceAccounts(context.Background(), &serviceaccounts.SearchOrgServiceAccountsQuery{OrgID: 1}) - require.Len(t, res.ServiceAccounts, 2) - require.NoError(t, err) - require.False(t, res.ServiceAccounts[0].IsExternal) - require.True(t, res.ServiceAccounts[1].IsExternal) - }) -} diff --git a/pkg/services/serviceaccounts/serviceaccounts.go b/pkg/services/serviceaccounts/serviceaccounts.go index ed889c47475..535e2b50625 100644 --- a/pkg/services/serviceaccounts/serviceaccounts.go +++ b/pkg/services/serviceaccounts/serviceaccounts.go @@ -13,13 +13,23 @@ Service accounts are used to authenticate API requests. They are not users and do not have a password. */ type Service interface { - AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *AddServiceAccountTokenCommand) (*apikey.APIKey, error) CreateServiceAccount(ctx context.Context, orgID int64, saForm *CreateServiceAccountForm) (*ServiceAccountDTO, error) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*ServiceAccountProfileDTO, error) RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) SearchOrgServiceAccounts(ctx context.Context, query *SearchOrgServiceAccountsQuery) (*SearchOrgServiceAccountsResult, error) - UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, saForm *UpdateServiceAccountForm) (*ServiceAccountProfileDTO, error) + UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, + saForm *UpdateServiceAccountForm) (*ServiceAccountProfileDTO, error) + + // Tokens + AddServiceAccountToken(ctx context.Context, serviceAccountID int64, + cmd *AddServiceAccountTokenCommand) (*apikey.APIKey, error) + DeleteServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error + ListTokens(ctx context.Context, query *GetSATokensQuery) ([]apikey.APIKey, error) + + // API specific functions + MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error + MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*MigrationResult, error) } //go:generate mockery --name ExtSvcAccountsService --structname MockExtSvcAccountsService --output tests --outpkg tests --filename extsvcaccmock.go diff --git a/pkg/services/serviceaccounts/tests/fakes.go b/pkg/services/serviceaccounts/tests/fakes.go new file mode 100644 index 00000000000..016c1ec9585 --- /dev/null +++ b/pkg/services/serviceaccounts/tests/fakes.go @@ -0,0 +1,69 @@ +package tests + +import ( + "context" + "fmt" + + "github.com/grafana/grafana/pkg/services/apikey" + "github.com/grafana/grafana/pkg/services/serviceaccounts" +) + +type FakeServiceAccountService struct { + ExpectedAPIKey *apikey.APIKey + ExpectedErr error + ExpectedMigrationResult *serviceaccounts.MigrationResult + ExpectedSearchOrgServiceAccountsResult *serviceaccounts.SearchOrgServiceAccountsResult + ExpectedServiceAccount *serviceaccounts.ServiceAccountDTO + ExpectedServiceAccountID int64 + ExpectedServiceAccountProfile *serviceaccounts.ServiceAccountProfileDTO + ExpectedServiceAccountTokens []apikey.APIKey +} + +var _ serviceaccounts.Service = new(FakeServiceAccountService) + +func (f *FakeServiceAccountService) AddServiceAccountToken(ctx context.Context, id int64, cmd *serviceaccounts.AddServiceAccountTokenCommand) (*apikey.APIKey, error) { + return f.ExpectedAPIKey, f.ExpectedErr +} + +func (f *FakeServiceAccountService) CreateServiceAccount(ctx context.Context, orgID int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { + return f.ExpectedServiceAccount, f.ExpectedErr +} + +func (f *FakeServiceAccountService) DeleteServiceAccount(ctx context.Context, orgID, id int64) error { + return f.ExpectedErr +} + +func (f *FakeServiceAccountService) RetrieveServiceAccount(ctx context.Context, orgID, id int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { + return f.ExpectedServiceAccountProfile, f.ExpectedErr +} + +func (f *FakeServiceAccountService) RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) { + return f.ExpectedServiceAccountID, f.ExpectedErr +} + +func (f *FakeServiceAccountService) UpdateServiceAccount(ctx context.Context, orgID, id int64, cmd *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) { + return f.ExpectedServiceAccountProfile, f.ExpectedErr +} + +func (f *FakeServiceAccountService) ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error) { + return f.ExpectedServiceAccountTokens, f.ExpectedErr +} + +func (f *FakeServiceAccountService) MigrateApiKey(ctx context.Context, orgID, keyID int64) error { + return f.ExpectedErr +} + +func (f *FakeServiceAccountService) MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error) { + fmt.Printf("fake migration result: %v", f.ExpectedMigrationResult) + return f.ExpectedMigrationResult, f.ExpectedErr +} + +func (f *FakeServiceAccountService) SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) { + return f.ExpectedSearchOrgServiceAccountsResult, f.ExpectedErr +} + +// Service account tokens + +func (f *FakeServiceAccountService) DeleteServiceAccountToken(ctx context.Context, orgID, id, tokenID int64) error { + return f.ExpectedErr +} diff --git a/pkg/services/serviceaccounts/tests/mocks.go b/pkg/services/serviceaccounts/tests/mocks.go index d7b5a30d03e..3e310802f9d 100644 --- a/pkg/services/serviceaccounts/tests/mocks.go +++ b/pkg/services/serviceaccounts/tests/mocks.go @@ -33,6 +33,30 @@ func (s *MockServiceAccountService) DeleteServiceAccount(ctx context.Context, or return mockedArgs.Error(0) } +// DeleteServiceAccountToken implements serviceaccounts.Service +func (s *MockServiceAccountService) DeleteServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error { + mockedArgs := s.Called(ctx, orgID, serviceAccountID, tokenID) + return mockedArgs.Error(0) +} + +// ListTokens implements serviceaccounts.Service +func (s *MockServiceAccountService) ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error) { + mockedArgs := s.Called(ctx, query) + return mockedArgs.Get(0).([]apikey.APIKey), mockedArgs.Error(1) +} + +// MigrateApiKey implements serviceaccounts.Service +func (s *MockServiceAccountService) MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error { + mockedArgs := s.Called(ctx, orgID, keyId) + return mockedArgs.Error(0) +} + +// MigrateApiKeysToServiceAccounts implements serviceaccounts.Service +func (s *MockServiceAccountService) MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error) { + mockedArgs := s.Called(ctx, orgID) + return mockedArgs.Get(0).(*serviceaccounts.MigrationResult), mockedArgs.Error(1) +} + // RetrieveServiceAccount implements serviceaccounts.Service func (s *MockServiceAccountService) RetrieveServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { mockedArgs := s.Called(ctx, orgID, serviceAccountID) @@ -45,13 +69,14 @@ func (s *MockServiceAccountService) RetrieveServiceAccountIdByName(ctx context.C return mockedArgs.Get(0).(int64), mockedArgs.Error(1) } +// SearchOrgServiceAccounts implements serviceaccounts.Service +func (s *MockServiceAccountService) SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) { + mockedArgs := s.Called(ctx, query) + return mockedArgs.Get(0).(*serviceaccounts.SearchOrgServiceAccountsResult), mockedArgs.Error(1) +} + // UpdateServiceAccount implements serviceaccounts.Service func (s *MockServiceAccountService) UpdateServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64, saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) { mockedArgs := s.Called(ctx, orgID, serviceAccountID) return mockedArgs.Get(0).(*serviceaccounts.ServiceAccountProfileDTO), mockedArgs.Error(1) } - -func (s *MockServiceAccountService) SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) { - mockedArgs := s.Called(ctx, query) - return mockedArgs.Get(0).(*serviceaccounts.SearchOrgServiceAccountsResult), mockedArgs.Error(1) -} From aa7a6da98522f3af18e037aaf7fdef9b813290cb Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 25 Oct 2023 13:03:12 +0200 Subject: [PATCH 052/180] Role picker: Fix flickering at service accounts page (#77049) * Role picker: Fix flickering at service accounts page * Set role picker fixed width * Fix betterer * Fix styles --- .betterer.results | 21 -- .../core/components/RolePicker/RolePicker.tsx | 18 +- .../components/RolePicker/RolePickerInput.tsx | 10 +- .../components/RolePicker/TeamRolePicker.tsx | 3 + .../components/RolePicker/UserRolePicker.tsx | 3 + .../app/core/components/RolePicker/styles.ts | 228 +++++++++--------- .../features/admin/Users/OrgUsersTable.tsx | 1 + .../ServiceAccountsListPage.tsx | 2 +- .../components/ServiceAccountsListItem.tsx | 1 + public/app/features/teams/TeamList.tsx | 2 +- 10 files changed, 144 insertions(+), 145 deletions(-) diff --git a/.betterer.results b/.betterer.results index d2db76d9161..e006165060f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1395,27 +1395,6 @@ exports[`better eslint`] = { "public/app/core/components/RolePicker/ValueContainer.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], - "public/app/core/components/RolePicker/styles.ts:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"], - [0, 0, 0, "Styles should be written using objects.", "6"], - [0, 0, 0, "Styles should be written using objects.", "7"], - [0, 0, 0, "Styles should be written using objects.", "8"], - [0, 0, 0, "Styles should be written using objects.", "9"], - [0, 0, 0, "Styles should be written using objects.", "10"], - [0, 0, 0, "Styles should be written using objects.", "11"], - [0, 0, 0, "Styles should be written using objects.", "12"], - [0, 0, 0, "Styles should be written using objects.", "13"], - [0, 0, 0, "Styles should be written using objects.", "14"], - [0, 0, 0, "Styles should be written using objects.", "15"], - [0, 0, 0, "Styles should be written using objects.", "16"], - [0, 0, 0, "Styles should be written using objects.", "17"], - [0, 0, 0, "Styles should be written using objects.", "18"] - ], "public/app/core/components/Select/OldFolderPicker.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] diff --git a/public/app/core/components/RolePicker/RolePicker.tsx b/public/app/core/components/RolePicker/RolePicker.tsx index bdbb78e432b..185d86187ff 100644 --- a/public/app/core/components/RolePicker/RolePicker.tsx +++ b/public/app/core/components/RolePicker/RolePicker.tsx @@ -1,11 +1,12 @@ import React, { FormEvent, useCallback, useEffect, useState, useRef } from 'react'; -import { ClickOutsideWrapper, HorizontalGroup, Spinner } from '@grafana/ui'; +import { ClickOutsideWrapper, Spinner, useStyles2, useTheme2 } from '@grafana/ui'; import { Role, OrgRole } from 'app/types'; import { RolePickerInput } from './RolePickerInput'; import { RolePickerMenu } from './RolePickerMenu'; import { MENU_MAX_HEIGHT, ROLE_PICKER_SUBMENU_MIN_WIDTH, ROLE_PICKER_WIDTH } from './constants'; +import { getStyles } from './styles'; export interface Props { basicRole?: OrgRole; @@ -24,6 +25,7 @@ export interface Props { */ apply?: boolean; maxWidth?: string | number; + width?: string | number; } export const RolePicker = ({ @@ -40,6 +42,7 @@ export const RolePicker = ({ canUpdateRoles = true, apply = false, maxWidth = ROLE_PICKER_WIDTH, + width, }: Props): JSX.Element | null => { const [isOpen, setOpen] = useState(false); const [selectedRoles, setSelectedRoles] = useState(appliedRoles); @@ -47,6 +50,9 @@ export const RolePicker = ({ const [query, setQuery] = useState(''); const [offset, setOffset] = useState({ vertical: 0, horizontal: 0 }); const ref = useRef(null); + const styles = useStyles2(getStyles); + const theme = useTheme2(); + const widthPx = typeof width === 'number' ? theme.spacing(width) : width; useEffect(() => { setSelectedBuiltInRole(basicRole); @@ -148,10 +154,10 @@ export const RolePicker = ({ if (isLoading) { return ( - +
Loading... - - + +
); } @@ -160,7 +166,8 @@ export const RolePicker = ({ data-testid="role-picker" style={{ position: 'relative', - maxWidth, + maxWidth: widthPx || maxWidth, + width: widthPx, }} ref={ref} > @@ -175,6 +182,7 @@ export const RolePicker = ({ isFocused={isOpen} disabled={disabled} showBasicRole={showBasicRole} + width={widthPx} /> {isOpen && ( { showBasicRole?: boolean; isFocused?: boolean; disabled?: boolean; + width?: string; onQueryChange: (query?: string) => void; onOpen: (event: FormEvent) => void; onClose: () => void; @@ -30,12 +31,13 @@ export const RolePickerInput = ({ isFocused, query, showBasicRole, + width, onOpen, onClose, onQueryChange, ...rest }: InputProps): JSX.Element => { - const styles = useStyles2(getRolePickerInputStyles, false, !!isFocused, !!disabled, false); + const styles = useStyles2(getRolePickerInputStyles, false, !!isFocused, !!disabled, false, width); const inputRef = useRef(null); useEffect(() => { @@ -125,7 +127,8 @@ const getRolePickerInputStyles = ( invalid: boolean, focused: boolean, disabled: boolean, - withPrefix: boolean + withPrefix: boolean, + width?: string ) => { const styles = getInputStyles({ theme, invalid }); @@ -139,7 +142,8 @@ const getRolePickerInputStyles = ( `, disabled && styles.inputDisabled, css` - min-width: ${ROLE_PICKER_WIDTH}px; + min-width: ${width || ROLE_PICKER_WIDTH + 'px'}; + width: ${width}; min-height: 32px; height: auto; flex-direction: row; diff --git a/public/app/core/components/RolePicker/TeamRolePicker.tsx b/public/app/core/components/RolePicker/TeamRolePicker.tsx index f4dec6622a9..98d292da3cf 100644 --- a/public/app/core/components/RolePicker/TeamRolePicker.tsx +++ b/public/app/core/components/RolePicker/TeamRolePicker.tsx @@ -27,6 +27,7 @@ export interface Props { */ apply?: boolean; maxWidth?: string | number; + width?: string | number; } export const TeamRolePicker = ({ @@ -37,6 +38,7 @@ export const TeamRolePicker = ({ pendingRoles, apply = false, maxWidth, + width, }: Props) => { const [{ loading, value: appliedRoles = [] }, getTeamRoles] = useAsyncFn(async () => { try { @@ -81,6 +83,7 @@ export const TeamRolePicker = ({ basicRoleDisabled={true} canUpdateRoles={canUpdateRoles} maxWidth={maxWidth} + width={width} /> ); }; diff --git a/public/app/core/components/RolePicker/UserRolePicker.tsx b/public/app/core/components/RolePicker/UserRolePicker.tsx index 73bac42b7d8..ca7cecdc07b 100644 --- a/public/app/core/components/RolePicker/UserRolePicker.tsx +++ b/public/app/core/components/RolePicker/UserRolePicker.tsx @@ -31,6 +31,7 @@ export interface Props { onApplyRoles?: (newRoles: Role[], userId: number, orgId: number | undefined) => void; pendingRoles?: Role[]; maxWidth?: string | number; + width?: string | number; } export const UserRolePicker = ({ @@ -46,6 +47,7 @@ export const UserRolePicker = ({ onApplyRoles, pendingRoles, maxWidth, + width, }: Props) => { const [{ loading, value: appliedRoles = [] }, getUserRoles] = useAsyncFn(async () => { try { @@ -98,6 +100,7 @@ export const UserRolePicker = ({ apply={apply} canUpdateRoles={canUpdateRoles} maxWidth={maxWidth} + width={width} /> ); }; diff --git a/public/app/core/components/RolePicker/styles.ts b/public/app/core/components/RolePicker/styles.ts index 4caed7800e0..74ce953af80 100644 --- a/public/app/core/components/RolePicker/styles.ts +++ b/public/app/core/components/RolePicker/styles.ts @@ -4,119 +4,119 @@ import { GrafanaTheme2 } from '@grafana/data'; import { ROLE_PICKER_SUBMENU_MIN_WIDTH } from './constants'; -export const getStyles = (theme: GrafanaTheme2) => { - return { - hideScrollBar: css` - .scrollbar-view { - /* Hide scrollbar for Chrome, Safari, and Opera */ - &::-webkit-scrollbar { - display: none; - } - /* Hide scrollbar for Firefox */ - scrollbar-width: none; - } - `, - menuWrapper: css` - display: flex; - max-height: 650px; - position: absolute; - z-index: ${theme.zIndex.dropdown}; - overflow: hidden; - min-width: auto; - `, - menu: css` - min-width: ${ROLE_PICKER_SUBMENU_MIN_WIDTH}px; +export const getStyles = (theme: GrafanaTheme2) => ({ + hideScrollBar: css({ + '.scrollbar-view': { + /* Hide scrollbar for Chrome, Safari, and Opera */ + '&::-webkit-scrollbar': { + display: 'none', + }, + /* Hide scrollbar for Firefox */ + scrollbarWidth: 'none', + }, + }), + menuWrapper: css({ + display: 'flex', + maxHeight: '650px', + position: 'absolute', + zIndex: theme.zIndex.dropdown, + overflow: 'hidden', + minWidth: 'auto', + }), + menu: css({ + minWidth: `${ROLE_PICKER_SUBMENU_MIN_WIDTH}px`, + '& > div': { + paddingTop: theme.spacing(1), + }, + }), + menuLeft: css({ + right: 0, + flexDirection: 'row-reverse', + }), + subMenu: css({ + height: '100%', + minWidth: `${ROLE_PICKER_SUBMENU_MIN_WIDTH}px`, + display: 'flex', + flexDirection: 'column', + borderLeft: `1px solid ${theme.components.input.borderColor}`, - & > div { - padding-top: ${theme.spacing(1)}; - } - `, - menuLeft: css` - right: 0; - flex-direction: row-reverse; - `, - subMenu: css` - height: 100%; - min-width: ${ROLE_PICKER_SUBMENU_MIN_WIDTH}px; - display: flex; - flex-direction: column; - border-left: 1px solid ${theme.components.input.borderColor}; + '& > div': { + paddingTop: theme.spacing(1), + }, + }), + subMenuLeft: css({ + borderRight: `1px solid ${theme.components.input.borderColor}`, + borderLeft: 'unset', + }), + groupHeader: css({ + padding: theme.spacing(0, 4.5), + display: 'flex', + alignItems: 'center', + color: theme.colors.text.primary, + fontWeight: theme.typography.fontWeightBold, + }), + container: css({ + padding: theme.spacing(1), + border: `1px ${theme.colors.border.weak} solid`, + borderRadius: theme.shape.radius.default, + backgroundColor: theme.colors.background.primary, + zIndex: theme.zIndex.modal, + }), + menuSection: css({ + marginBottom: theme.spacing(2), + }), + menuOptionCheckbox: css({ + display: 'flex', + margin: theme.spacing(0, 1, 0, 0.25), + }), + menuButtonRow: css({ + backgroundColor: theme.colors.background.primary, + padding: theme.spacing(1), + }), + menuOptionBody: css({ + fontWeight: theme.typography.fontWeightRegular, + padding: theme.spacing(0, 1.5, 0, 0), + }), + menuOptionDisabled: css({ + color: theme.colors.text.disabled, + cursor: 'not-allowed', + }), + menuOptionExpand: css({ + position: 'absolute', + right: theme.spacing(1.25), + color: theme.colors.text.disabled, - & > div { - padding-top: ${theme.spacing(1)}; - } - `, - subMenuLeft: css` - border-right: 1px solid ${theme.components.input.borderColor}; - border-left: unset; - `, - groupHeader: css` - padding: ${theme.spacing(0, 4.5)}; - display: flex; - align-items: center; - color: ${theme.colors.text.primary}; - font-weight: ${theme.typography.fontWeightBold}; - `, - container: css` - padding: ${theme.spacing(1)}; - border: 1px ${theme.colors.border.weak} solid; - border-radius: ${theme.shape.radius.default}; - background-color: ${theme.colors.background.primary}; - z-index: ${theme.zIndex.modal}; - `, - menuSection: css` - margin-bottom: ${theme.spacing(2)}; - `, - menuOptionCheckbox: css` - display: flex; - margin: ${theme.spacing(0, 1, 0, 0.25)}; - `, - menuButtonRow: css` - background-color: ${theme.colors.background.primary}; - padding: ${theme.spacing(1)}; - `, - menuOptionBody: css` - font-weight: ${theme.typography.fontWeightRegular}; - padding: ${theme.spacing(0, 1.5, 0, 0)}; - `, - menuOptionDisabled: css` - color: ${theme.colors.text.disabled}; - cursor: not-allowed; - `, - menuOptionExpand: css` - position: absolute; - right: ${theme.spacing(1.25)}; - color: ${theme.colors.text.disabled}; - - &:after { - content: '>'; - } - `, - menuOptionInfoSign: css` - color: ${theme.colors.text.disabled}; - `, - basicRoleSelector: css` - margin: ${theme.spacing(1, 1.25, 1, 1.5)}; - `, - subMenuPortal: css` - height: 100%; - > div { - height: 100%; - } - `, - subMenuButtonRow: css` - background-color: ${theme.colors.background.primary}; - padding: ${theme.spacing(1)}; - `, - checkboxPartiallyChecked: css` - input { - &:checked + span { - &:after { - border-width: 0 3px 0px 0; - transform: rotate(90deg); - } - } - } - `, - }; -}; + '&:after': { + content: '">"', + }, + }), + menuOptionInfoSign: css({ + color: theme.colors.text.disabled, + }), + basicRoleSelector: css({ + margin: theme.spacing(1, 1.25, 1, 1.5), + }), + subMenuPortal: css({ + height: '100%', + '> div': { + height: '100%', + }, + }), + subMenuButtonRow: css({ + backgroundColor: theme.colors.background.primary, + padding: theme.spacing(1), + }), + checkboxPartiallyChecked: css({ + input: { + '&:checked + span': { + '&:after': { + borderWidth: '0 3px 0px 0', + transform: 'rotate(90deg)', + }, + }, + }, + }), + loadingSpinner: css({ + marginLeft: theme.spacing(1), + }), +}); diff --git a/public/app/features/admin/Users/OrgUsersTable.tsx b/public/app/features/admin/Users/OrgUsersTable.tsx index bb71f21a66e..b4a6c229ea6 100644 --- a/public/app/features/admin/Users/OrgUsersTable.tsx +++ b/public/app/features/admin/Users/OrgUsersTable.tsx @@ -132,6 +132,7 @@ export const OrgUsersTable = ({ onBasicRoleChange={(newRole) => onRoleChange(newRole, original)} basicRoleDisabled={basicRoleDisabled} basicRoleDisabledMessage={disabledRoleMessage} + width={40} /> ) : ( ID Roles Tokens - + diff --git a/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx b/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx index d0c21a74e60..2d10943a4d5 100644 --- a/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx +++ b/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx @@ -81,6 +81,7 @@ const ServiceAccountListItem = memo( roleOptions={roleOptions} basicRoleDisabled={!canUpdateRole} disabled={serviceAccount.isDisabled} + width={40} /> )} diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 435ab51fa02..fca388998f8 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -96,7 +96,7 @@ export const TeamList = ({ AccessControlAction.ActionTeamsRolesList, original ); - return canSeeTeamRoles && ; + return canSeeTeamRoles && ; }, }, ] From 322a9c0b15f5b655d3e58b1dbc201061903589fe Mon Sep 17 00:00:00 2001 From: Santiago Date: Wed, 25 Oct 2023 13:58:28 +0200 Subject: [PATCH 053/180] Alerting: Replace FileStore() for CleanUp() in the Alertmanager interface (#77126) Alerting: Remplace FileStore() for CleanUp() in the Alertmanager interface --- pkg/services/ngalert/notifier/alertmanager.go | 5 +++-- pkg/services/ngalert/notifier/multiorg_alertmanager.go | 8 ++++---- pkg/services/ngalert/remote/alertmanager.go | 5 ++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 8c138fd8638..38d43d850df 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -403,8 +403,9 @@ func (am *alertmanager) OrgID() int64 { return am.orgID } -func (am *alertmanager) FileStore() *FileStore { - return am.fileStore +// CleanUp removes the directory containing the alertmanager files from disk. +func (am *alertmanager) CleanUp() { + am.fileStore.CleanUp() } // AlertValidationError is the error capturing the validation errors diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager.go b/pkg/services/ngalert/notifier/multiorg_alertmanager.go index ea635cd36d1..aa3ac45b838 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager.go @@ -54,9 +54,9 @@ type Alertmanager interface { TestTemplate(ctx context.Context, c apimodels.TestTemplatesConfigBodyParams) (*TestTemplatesResults, error) // State + CleanUp() StopAndWait() Ready() bool - FileStore() *FileStore OrgID() int64 ConfigHash() [16]byte } @@ -314,8 +314,8 @@ func (moa *MultiOrgAlertmanager) SyncAlertmanagersForOrgs(ctx context.Context, o moa.logger.Info("Stopping Alertmanager", "org", orgID) am.StopAndWait() moa.logger.Info("Stopped Alertmanager", "org", orgID) - // Cleanup all the remaining resources from this alertmanager. - am.FileStore().CleanUp() + // Clean up all the remaining resources from this alertmanager. + am.CleanUp() } // We look for orphan directories and remove them. Orphan directories can @@ -350,7 +350,7 @@ func (moa *MultiOrgAlertmanager) cleanupOrphanLocalOrgState(ctx context.Context, moa.logger.Info("Found orphan organization directory", "orgID", orgID) workingDirPath := filepath.Join(dataDir, strconv.FormatInt(orgID, 10)) fileStore := NewFileStore(orgID, moa.kvStore, workingDirPath) - // Cleanup all the remaining resources from this alertmanager. + // Clean up all the remaining resources from this alertmanager. fileStore.CleanUp() } } diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 073400a333d..ab68702182f 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -294,9 +294,8 @@ func (am *Alertmanager) Ready() bool { return am.ready } -func (am *Alertmanager) FileStore() *notifier.FileStore { - return ¬ifier.FileStore{} -} +// We don't have files on disk, no-op. +func (am *Alertmanager) CleanUp() {} func (am *Alertmanager) OrgID() int64 { return am.orgID From 9bf7eb5fbc1ffb48fcae088742c445d562655592 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 25 Oct 2023 14:01:30 +0200 Subject: [PATCH 054/180] Plugins: Adds logging around loading of plugins for better tracking (#76896) --- pkg/plugins/log/logger.go | 1 - pkg/plugins/manager/loader/loader.go | 26 +++++++++++++++++++ .../pluginsintegration/pluginstore/store.go | 16 +++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/pkg/plugins/log/logger.go b/pkg/plugins/log/logger.go index 58ac225cf64..f0eed4f3e07 100644 --- a/pkg/plugins/log/logger.go +++ b/pkg/plugins/log/logger.go @@ -23,7 +23,6 @@ func (d *grafanaInfraLogWrapper) New(ctx ...any) Logger { } } - ctx = append([]any{"logger"}, ctx...) return &grafanaInfraLogWrapper{ l: d.l.New(ctx...), } diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index 78e31041dcd..c6b85bbb5f4 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -2,6 +2,9 @@ package loader import ( "context" + "sort" + "strings" + "time" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/log" @@ -34,6 +37,8 @@ func New(discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, valid } func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { + end := l.instrumentLoad(ctx, src) + discoveredPlugins, err := l.discovery.Discover(ctx, src) if err != nil { return nil, err @@ -54,9 +59,30 @@ func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins return nil, err } + end(initializedPlugins) + return initializedPlugins, nil } func (l *Loader) Unload(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { return l.termination.Terminate(ctx, p) } + +func (l *Loader) instrumentLoad(ctx context.Context, src plugins.PluginSource) func([]*plugins.Plugin) { + start := time.Now() + sourceLogger := l.log.New("source", src.PluginClass(ctx)).FromContext(ctx) + sourceLogger.Debug("Loading plugin source...") + + return func(logger log.Logger, start time.Time) func([]*plugins.Plugin) { + return func(plugins []*plugins.Plugin) { + names := make([]string, len(plugins)) + for i, p := range plugins { + names[i] = p.ID + } + sort.Strings(names) + pluginsStr := strings.Join(names, ", ") + + logger.Debug("Plugin source loaded", "plugins", pluginsStr, "duration", time.Since(start)) + } + }(sourceLogger, start) +} diff --git a/pkg/services/pluginsintegration/pluginstore/store.go b/pkg/services/pluginsintegration/pluginstore/store.go index ea02358f4b9..3fd4cadec59 100644 --- a/pkg/services/pluginsintegration/pluginstore/store.go +++ b/pkg/services/pluginsintegration/pluginstore/store.go @@ -4,7 +4,9 @@ import ( "context" "sort" "sync" + "time" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager/loader" "github.com/grafana/grafana/pkg/plugins/manager/registry" @@ -29,11 +31,23 @@ type Service struct { func ProvideService(pluginRegistry registry.Service, pluginSources sources.Registry, pluginLoader loader.Service) (*Service, error) { ctx := context.Background() + start := time.Now() + totalPlugins := 0 + logger := log.New("plugin.store") + logger.Info("Loading plugins...") + for _, ps := range pluginSources.List(ctx) { - if _, err := pluginLoader.Load(ctx, ps); err != nil { + loadedPlugins, err := pluginLoader.Load(ctx, ps) + if err != nil { + logger.Error("Loading plugin source failed", "source", ps.PluginClass(ctx), "error", err) return nil, err } + + totalPlugins += len(loadedPlugins) } + + logger.Info("Plugins loaded", "count", totalPlugins, "duration", time.Since(start)) + return New(pluginRegistry, pluginLoader), nil } From 20fc0cbf35c58a29f22c7b5e66afb67c3993a861 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Wed, 25 Oct 2023 14:28:12 +0200 Subject: [PATCH 055/180] Chore: Allow env overrides for the `extended_jwt` config (#77132) Chore: Allow env overrides for the extended_jwt config --- pkg/setting/setting.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 75c72d471b4..1983bfbb2f9 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -1630,7 +1630,7 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { cfg.JWTAuthSkipOrgRoleSync = authJWT.Key("skip_org_role_sync").MustBool(false) // Extended JWT auth - authExtendedJWT := iniFile.Section("auth.extended_jwt") + authExtendedJWT := cfg.SectionWithEnvOverrides("auth.extended_jwt") cfg.ExtendedJWTAuthEnabled = authExtendedJWT.Key("enabled").MustBool(false) cfg.ExtendedJWTExpectAudience = authExtendedJWT.Key("expect_audience").MustString("") cfg.ExtendedJWTExpectIssuer = authExtendedJWT.Key("expect_issuer").MustString("") From 327ae398e6438de7d8b76e55af6ae10d82749104 Mon Sep 17 00:00:00 2001 From: Kuba Siemiatkowski <112862936+sasklacz@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:02:03 +0200 Subject: [PATCH 056/180] Add SumoLogic plugin (#77025) - add Sumo to plugins list - remove duplicated sqlyze from the plugins list --- .../introduction/grafana-enterprise.md | 4 ++-- .../datasources/state/buildCategories.test.ts | 2 +- .../datasources/state/buildCategories.ts | 6 ++++++ public/img/plugins/sumo.svg | 21 +++++++++++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 public/img/plugins/sumo.svg diff --git a/docs/sources/introduction/grafana-enterprise.md b/docs/sources/introduction/grafana-enterprise.md index e7ea3c27e3b..2de825d6c57 100644 --- a/docs/sources/introduction/grafana-enterprise.md +++ b/docs/sources/introduction/grafana-enterprise.md @@ -84,7 +84,6 @@ With a Grafana Enterprise license, you also get access to premium data sources, - [k6 Cloud App](/grafana/plugins/grafana-k6-app) - [MongoDB](/grafana/plugins/grafana-mongodb-datasource) - [New Relic](/grafana/plugins/grafana-newrelic-datasource) -- [Sqlyze Datasource](/grafana/plugins/grafana-odbc-datasource) - [Oracle Database](/grafana/plugins/grafana-oracle-datasource) - [Salesforce](/grafana/plugins/grafana-salesforce-datasource) - [SAP HANA®](/grafana/plugins/grafana-saphana-datasource) @@ -92,7 +91,8 @@ With a Grafana Enterprise license, you also get access to premium data sources, - [Snowflake](/grafana/plugins/grafana-snowflake-datasource) - [Splunk](/grafana/plugins/grafana-splunk-datasource) - [Splunk Infrastructure monitoring (SignalFx)](/grafana/plugins/grafana-splunk-monitoring-datasource) -- [Sqlyze](/grafana/plugins/grafana-odbc-datasource/) +- [Sqlyze Datasource](/grafana/plugins/grafana-odbc-datasource) +- [SumoLogic](/grafana/plugins/grafana-sumologic-datasource) - [Wavefront](/grafana/plugins/grafana-wavefront-datasource) ## Try Grafana Enterprise diff --git a/public/app/features/datasources/state/buildCategories.test.ts b/public/app/features/datasources/state/buildCategories.test.ts index 4a4e8d44c2d..d29e5839d72 100644 --- a/public/app/features/datasources/state/buildCategories.test.ts +++ b/public/app/features/datasources/state/buildCategories.test.ts @@ -53,7 +53,7 @@ describe('buildCategories', () => { it('should add enterprise phantom plugins', () => { const enterprisePluginsCategory = categories[3]; expect(enterprisePluginsCategory.title).toBe('Enterprise plugins'); - expect(enterprisePluginsCategory.plugins.length).toBe(17); + expect(enterprisePluginsCategory.plugins.length).toBe(18); expect(enterprisePluginsCategory.plugins[0].name).toBe('AppDynamics'); expect(enterprisePluginsCategory.plugins[enterprisePluginsCategory.plugins.length - 1].name).toBe('Wavefront'); }); diff --git a/public/app/features/datasources/state/buildCategories.ts b/public/app/features/datasources/state/buildCategories.ts index 68fa9f45d2d..bab9d9011a7 100644 --- a/public/app/features/datasources/state/buildCategories.ts +++ b/public/app/features/datasources/state/buildCategories.ts @@ -197,6 +197,12 @@ function getEnterprisePhantomPlugins(): DataSourcePluginMeta[] { name: 'Azure Devops', imgUrl: 'public/img/plugins/azure-devops.png', }), + getPhantomPlugin({ + id: 'grafana-sumologic-datasource', + description: 'SumoLogic integration and datasource', + name: 'SumoLogic', + imgUrl: 'public/img/plugins/sumo.svg', + }), ]; } diff --git a/public/img/plugins/sumo.svg b/public/img/plugins/sumo.svg new file mode 100644 index 00000000000..3f75c3a027d --- /dev/null +++ b/public/img/plugins/sumo.svg @@ -0,0 +1,21 @@ + + + + + Asset 1 + + + + + + + \ No newline at end of file From b215d2f0fb2916458f15fbc31e64139d5d9dcaf8 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Wed, 25 Oct 2023 14:29:57 +0100 Subject: [PATCH 057/180] Library Panels: Fix library panel creation with RBAC enabled (#76553) --- pkg/services/libraryelements/api.go | 2 ++ .../AddLibraryPanelModal/AddLibraryPanelModal.tsx | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go index 5bb3a03612a..d3113c48526 100644 --- a/pkg/services/libraryelements/api.go +++ b/pkg/services/libraryelements/api.go @@ -63,6 +63,8 @@ func (l *LibraryElementService) createHandler(c *contextmodel.ReqContext) respon if cmd.FolderUID != nil { if *cmd.FolderUID == "" { cmd.FolderID = 0 + generalFolderUID := ac.GeneralFolderUID + cmd.FolderUID = &generalFolderUID } else { folder, err := l.folderService.Get(c.Req.Context(), &folder.GetFolderQuery{OrgID: c.SignedInUser.GetOrgID(), UID: cmd.FolderUID, SignedInUser: c.SignedInUser}) if err != nil || folder == nil { diff --git a/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx b/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx index a982f9e7342..d4b9f8db378 100644 --- a/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx +++ b/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx @@ -1,13 +1,14 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useAsync, useDebounce } from 'react-use'; -import { isFetchError } from '@grafana/runtime'; +import { FetchError, isFetchError } from '@grafana/runtime'; import { Button, Field, Input, Modal } from '@grafana/ui'; import { OldFolderPicker } from 'app/core/components/Select/OldFolderPicker'; import { t, Trans } from 'app/core/internationalization'; import { PanelModel } from '../../../dashboard/state'; import { getLibraryPanelByName } from '../../state/api'; +import { LibraryElementDTO } from '../../types'; import { usePanelSave } from '../../utils/usePanelSave'; interface AddLibraryPanelContentsProps { @@ -28,9 +29,11 @@ export const AddLibraryPanelContents = ({ panel, initialFolderUid, onDismiss }: const { saveLibraryPanel } = usePanelSave(); const onCreate = useCallback(() => { panel.libraryPanel = { uid: '', name: panelName }; - saveLibraryPanel(panel, folderUid!).then((res) => { - if (!(res instanceof Error)) { + saveLibraryPanel(panel, folderUid!).then((res: LibraryElementDTO | FetchError) => { + if (!isFetchError(res)) { onDismiss(); + } else { + panel.libraryPanel = undefined; } }); }, [panel, panelName, folderUid, onDismiss, saveLibraryPanel]); From 2a43ee5d4603c5b8c388193ee50a545a878d6f35 Mon Sep 17 00:00:00 2001 From: Isabel <76437239+imatwawana@users.noreply.github.com> Date: Wed, 25 Oct 2023 09:35:23 -0400 Subject: [PATCH 058/180] Docs: edit export alerting resources feature in Cloud what's new (#76997) * Added Alerting features * Apply suggestions from code review * Moved feature into order by date * Added updated Terraform description and removed provisioned resources feature --- docs/sources/whatsnew/whats-new-next/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/whatsnew/whats-new-next/index.md b/docs/sources/whatsnew/whats-new-next/index.md index da88b614f55..fb9b5d2d766 100644 --- a/docs/sources/whatsnew/whats-new-next/index.md +++ b/docs/sources/whatsnew/whats-new-next/index.md @@ -18,7 +18,7 @@ weight: -37 Welcome to Grafana Cloud! Read on to learn about the newest changes to Grafana Cloud. -## Export alert rules and notification resources to Terraform +## Export alerting resources to Terraform @@ -27,7 +27,7 @@ October 30, 2023 _Generally available in Grafana Cloud_ -This feature provides a way to export Alerting resources such as rules, contact points, and notification policies as Terraform resources. A new "Modify export" mode for alert rules provides a convenient way of editing provisioned alert rules and exporting the modified version. +Export your alerting resources, such as alert rules, contact points, and notification policies as Terraform resources. A new “Modify export” mode for alert rules enables you to edit provisioned alert rules and export a modified version. ## Alerting insights From 85468d2a67638f8930b67e50b826403f21e1f38d Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 25 Oct 2023 14:41:07 +0100 Subject: [PATCH 059/180] DockedMegaMenu: Adjust docked threshold (#77139) * adjust docked threshold to be xl instead of md and make sure it's open by default on 1440 res * don't show dock menu button --- public/app/core/components/AppChrome/AppChrome.tsx | 10 +++++----- .../app/core/components/AppChrome/AppChromeService.tsx | 2 +- .../components/AppChrome/DockedMegaMenu/MegaMenu.tsx | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx index 4dcc1c114db..b73009538a6 100644 --- a/public/app/core/components/AppChrome/AppChrome.tsx +++ b/public/app/core/components/AppChrome/AppChrome.tsx @@ -41,10 +41,10 @@ export function AppChrome({ children }: Props) { chrome.setMegaMenu('closed'); break; case 'docked': - // on desktop, clicking the button when the menu is docked should close the menu - // on mobile, the docked menu is hidden, so clicking the button should open the menu - const isDesktop = window.innerWidth > theme.breakpoints.values.md; - isDesktop ? chrome.setMegaMenu('closed') : chrome.setMegaMenu('open'); + // on large screens, clicking the button when the menu is docked should close the menu + // on smaller screens, the docked menu is hidden, so clicking the button should open the menu + const isLargeScreen = window.innerWidth >= theme.breakpoints.values.xl; + isLargeScreen ? chrome.setMegaMenu('closed') : chrome.setMegaMenu('open'); break; } }; @@ -131,7 +131,7 @@ const getStyles = (theme: GrafanaTheme2) => { display: 'none', zIndex: theme.zIndex.navbarFixed, - [theme.breakpoints.up('md')]: { + [theme.breakpoints.up('xl')]: { display: 'block', }, }), diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx index 9e19c5d0f16..83032e0dd63 100644 --- a/public/app/core/components/AppChrome/AppChromeService.tsx +++ b/public/app/core/components/AppChrome/AppChromeService.tsx @@ -35,7 +35,7 @@ export class AppChromeService { searchBarHidden: store.getBool(this.searchBarStorageKey, false), megaMenu: config.featureToggles.dockedMegaMenu && - store.getBool(DOCKED_LOCAL_STORAGE_KEY, window.innerWidth > config.theme2.breakpoints.values.xxl) + store.getBool(DOCKED_LOCAL_STORAGE_KEY, window.innerWidth >= config.theme2.breakpoints.values.xxl) ? 'docked' : 'closed', kioskMode: null, diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx index ca3a90e364e..3f615d45a8b 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx @@ -117,7 +117,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ dockMenuButton: css({ display: 'none', - [theme.breakpoints.up('md')]: { + [theme.breakpoints.up('xl')]: { display: 'inline-flex', }, }), From ba9c22f51be4a6f35178e48e717e1d1f79723406 Mon Sep 17 00:00:00 2001 From: Isabel <76437239+imatwawana@users.noreply.github.com> Date: Wed, 25 Oct 2023 09:51:31 -0400 Subject: [PATCH 060/180] Add separate token handling for OAuth providers (#76461) * Add separate token handling for OAuth providers * Fixed version syntax * Added release date --------- Co-authored-by: Mihaly Gyongyosi --- docs/sources/whatsnew/whats-new-next/index.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/sources/whatsnew/whats-new-next/index.md b/docs/sources/whatsnew/whats-new-next/index.md index fb9b5d2d766..743f5457d0c 100644 --- a/docs/sources/whatsnew/whats-new-next/index.md +++ b/docs/sources/whatsnew/whats-new-next/index.md @@ -39,6 +39,27 @@ _Generally available in Grafana Cloud_ Use Alerting insights to monitor your alerting data, discover key trends about your organization’s alert management performance, and find patterns in why things go wrong. +## Configure refresh token handling separately for OAuth providers + + + + +October 24, 2023 + +_Generally available in Grafana Cloud_ + +With Grafana v9.3, we introduced a feature toggle called `accessTokenExpirationCheck`. It improves the security of Grafana by checking the expiration of the access token and automatically refreshing the expired access token when a user is logged in using one of the OAuth providers. + +With the current release, we've introduced a new configuration option for each OAuth provider called `use_refresh_token` that allows you to configure whether the particular OAuth integration should use refresh tokens to automatically refresh access tokens when they expire. In addition, to further improve security and provide secure defaults, `use_refresh_token` is enabled by default for providers that support either refreshing tokens automatically or client-controlled fetching of refresh tokens. It's enabled by default for the following OAuth providers: `AzureAD`, `GitLab`, `Google`. + +For more information on how to set up refresh token handling, please refer to [the documentation of the particular OAuth provider.](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/). + +{{% admonition type="note" %}} +The `use_refresh_token` configuration must be used in conjunction with the `accessTokenExpirationCheck` feature toggle. If you disable the `accessTokenExpirationCheck` feature toggle, Grafana won't check the expiration of the access token and won't automatically refresh the expired access token, even if the `use_refresh_token` configuration is set to `true`. + +The `accessTokenExpirationCheck` feature toggle will be removed in Grafana v10.3. +{{% /admonition %}} + ## Use AI to generate dashboard titles, descriptions, and change summaries From 283f279a1740b8471a31c6e0443df139133c114e Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Wed, 25 Oct 2023 15:56:10 +0200 Subject: [PATCH 061/180] InfluxDB: Fix adhoc filter calls by properly checking optional parameter in metricFindQuery (#77113) * Handle optional options parameter * unit tests --- .../datasource/influxdb/datasource.test.ts | 51 +++++++++++++++++++ .../plugins/datasource/influxdb/datasource.ts | 4 +- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.test.ts b/public/app/plugins/datasource/influxdb/datasource.test.ts index 7f5831a6ad0..6462fbd09b2 100644 --- a/public/app/plugins/datasource/influxdb/datasource.test.ts +++ b/public/app/plugins/datasource/influxdb/datasource.test.ts @@ -161,6 +161,9 @@ describe('InfluxDataSource Frontend Mode', () => { const mockTemplateService = new TemplateSrv(); mockTemplateService.getAdhocFilters = jest.fn((_: string) => adhocFilters); let ds = getMockInfluxDS(getMockDSInstanceSettings(), mockTemplateService); + + // const fetchMock = jest.fn().mockReturnValue(fetchResult); + it('query should contain the ad-hoc variable', () => { ds.query(mockInfluxQueryRequest()); const expected = encodeURIComponent( @@ -168,6 +171,54 @@ describe('InfluxDataSource Frontend Mode', () => { ); expect(fetchMock.mock.calls[0][0].data).toBe(`q=${expected}`); }); + + it('should make the fetch call for adhoc filter keys', () => { + fetchMock.mockReturnValue( + of({ + results: [ + { + statement_id: 0, + series: [ + { + name: 'cpu', + columns: ['tagKey'], + values: [['datacenter'], ['geohash'], ['source']], + }, + ], + }, + ], + }) + ); + ds.getTagKeys(); + expect(fetchMock).toHaveBeenCalled(); + const fetchReq = fetchMock.mock.calls[0][0]; + expect(fetchReq).not.toBeNull(); + expect(fetchReq.data).toMatch(encodeURIComponent(`SHOW TAG KEYS`)); + }); + + it('should make the fetch call for adhoc filter values', () => { + fetchMock.mockReturnValue( + of({ + results: [ + { + statement_id: 0, + series: [ + { + name: 'mykey', + columns: ['key', 'value'], + values: [['mykey', 'value']], + }, + ], + }, + ], + }) + ); + ds.getTagValues({ key: 'mykey', filters: [] }); + expect(fetchMock).toHaveBeenCalled(); + const fetchReq = fetchMock.mock.calls[0][0]; + expect(fetchReq).not.toBeNull(); + expect(fetchReq.data).toMatch(encodeURIComponent(`SHOW TAG VALUES WITH KEY = "mykey"`)); + }); }); describe('datasource contract', () => { diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 246ecdc9ec7..22e501696d8 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -313,7 +313,7 @@ export default class InfluxDatasource extends DataSourceWithBackend { @@ -324,7 +324,7 @@ export default class InfluxDatasource extends DataSourceWithBackend { return this.responseParser.parse(query, resp); From e12e40fc2493160338237b0b94e72fa530a78ef4 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Wed, 25 Oct 2023 15:57:53 +0200 Subject: [PATCH 062/180] Alerting: Contact Points v2 part IV (#76063) --- .betterer.results | 4 - .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + public/app/features/alerting/routes.tsx | 12 + .../features/alerting/unified/Receivers.tsx | 14 +- .../GrafanaAlertmanagerDeliveryWarning.tsx | 8 +- .../unified/components/MoreButton.tsx | 24 ++ .../contact-points/ContactPoints.v2.test.tsx | 131 ++++---- .../contact-points/ContactPoints.v2.tsx | 299 +++++++++++------- .../__mocks__/grafanaManagedServer.ts | 21 +- .../__mocks__/mimirFlavoredServer.ts | 4 +- .../useContactPoints.test.tsx.snap | 25 ++ .../contact-points/useContactPoints.test.tsx | 20 +- .../contact-points/useContactPoints.tsx | 55 +++- .../components/contact-points/utils.ts | 60 +++- .../components/export/FileExportPreview.tsx | 5 +- .../ContactPointSelector.tsx | 56 ++++ .../components/receivers/EditReceiverView.tsx | 23 +- .../receivers/ReceiversTable.test.tsx | 4 +- .../components/receivers/ReceiversTable.tsx | 4 +- .../receivers/form/CloudReceiverForm.tsx | 6 +- .../receivers/form/GrafanaReceiverForm.tsx | 13 +- .../ReceiverMetadataBadge.tsx | 47 ++- .../useReceiversMetadata.ts | 56 ++-- .../app/features/alerting/unified/features.ts | 4 - 28 files changed, 610 insertions(+), 299 deletions(-) create mode 100644 public/app/features/alerting/unified/components/MoreButton.tsx create mode 100644 public/app/features/alerting/unified/components/notification-policies/ContactPointSelector.tsx diff --git a/.betterer.results b/.betterer.results index e006165060f..bc0c24edcd0 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2228,10 +2228,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "3"], [0, 0, 0, "Styles should be written using objects.", "4"] ], - "public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/ReceiverMetadataBadge.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] - ], "public/app/features/alerting/unified/components/rule-editor/AnnotationKeyInput.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 295f4d8e7cf..7e091a115e2 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -80,6 +80,7 @@ Some features are enabled by default. You can disable these feature by setting t | `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the `useCachingService` feature toggle is enabled and the datasource has caching and async query support enabled | | `splitScopes` | Support faster dashboard and folder search by splitting permission scopes into parts | | `reportingRetries` | Enables rendering retries for the reporting feature | +| `alertingContactPointsV2` | Show the new contacpoints list view | | `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches | ## Experimental feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index f471b2d9b9a..8d905e8bfc1 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -128,6 +128,7 @@ export interface FeatureToggles { lokiRunQueriesInParallel?: boolean; wargamesTesting?: boolean; alertingInsights?: boolean; + alertingContactPointsV2?: boolean; externalCorePlugins?: boolean; pluginsAPIMetrics?: boolean; httpSLOLevels?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 70fd828c1c8..d12d46d9dfe 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -773,6 +773,13 @@ var ( Owner: grafanaAlertingSquad, Expression: "true", // enabled by default }, + { + Name: "alertingContactPointsV2", + Description: "Show the new contacpoints list view", + FrontendOnly: true, + Stage: FeatureStagePublicPreview, + Owner: grafanaAlertingSquad, + }, { Name: "externalCorePlugins", Description: "Allow core plugins to be loaded as external", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 570056669d2..d94647febe3 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -109,6 +109,7 @@ libraryPanelRBAC,experimental,@grafana/dashboards-squad,false,false,true,false lokiRunQueriesInParallel,privatePreview,@grafana/observability-logs,false,false,false,false wargamesTesting,experimental,@grafana/hosted-grafana-team,false,false,false,false alertingInsights,GA,@grafana/alerting-squad,false,false,false,true +alertingContactPointsV2,preview,@grafana/alerting-squad,false,false,false,true externalCorePlugins,experimental,@grafana/plugins-platform-backend,false,false,false,false pluginsAPIMetrics,experimental,@grafana/plugins-platform-backend,false,false,false,true httpSLOLevels,experimental,@grafana/hosted-grafana-team,false,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 299df484a21..7cdc48e682a 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -447,6 +447,10 @@ const ( // Show the new alerting insights landing page FlagAlertingInsights = "alertingInsights" + // FlagAlertingContactPointsV2 + // Show the new contacpoints list view + FlagAlertingContactPointsV2 = "alertingContactPointsV2" + // FlagExternalCorePlugins // Allow core plugins to be loaded as external FlagExternalCorePlugins = "externalCorePlugins" diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index 5e67be4822d..4ae31262daa 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -187,6 +187,18 @@ const unifiedRoutes: RouteDescriptor[] = [ () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') ), }, + { + path: '/alerting/notifications/receivers/:id/edit', + roles: evaluateAccess([ + AccessControlAction.AlertingNotificationsWrite, + AccessControlAction.AlertingNotificationsExternalWrite, + AccessControlAction.AlertingNotificationsRead, + AccessControlAction.AlertingNotificationsExternalRead, + ]), + component: SafeDynamicImport( + () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') + ), + }, { path: '/alerting/notifications/:type/:id/edit', roles: evaluateAccess([ diff --git a/public/app/features/alerting/unified/Receivers.tsx b/public/app/features/alerting/unified/Receivers.tsx index 4b00dc040ff..ccaa9128c96 100644 --- a/public/app/features/alerting/unified/Receivers.tsx +++ b/public/app/features/alerting/unified/Receivers.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { Disable, Enable } from 'react-enable'; import { Route, Switch } from 'react-router-dom'; +import { config } from '@grafana/runtime'; import { withErrorBoundary } from '@grafana/ui'; const ContactPointsV1 = SafeDynamicImport(() => import('./components/contact-points/ContactPoints.v1')); const ContactPointsV2 = SafeDynamicImport(() => import('./components/contact-points/ContactPoints.v2')); @@ -17,13 +17,14 @@ import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynami import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper'; -import { AlertingFeature } from './features'; + +const newContactPointsListView = config.featureToggles.alertingContactPointsV2 ?? false; // TODO add pagenav back in – that way we have correct breadcrumbs and page title const ContactPoints = (props: GrafanaRouteComponentProps): JSX.Element => ( - - {/* TODO do we want a "routes" component for each Alerting entity? */} + {/* TODO do we want a "routes" component for each Alerting entity? */} + {newContactPointsListView ? ( @@ -37,10 +38,9 @@ const ContactPoints = (props: GrafanaRouteComponentProps): JSX.Element => ( /> - - + ) : ( - + )} ); diff --git a/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx b/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx index 7d1306ccfad..9be6b64f41a 100644 --- a/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx +++ b/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx @@ -14,12 +14,12 @@ interface GrafanaAlertmanagerDeliveryWarningProps { export function GrafanaAlertmanagerDeliveryWarning({ currentAlertmanager }: GrafanaAlertmanagerDeliveryWarningProps) { const styles = useStyles2(getStyles); - - const { useGetAlertmanagerChoiceStatusQuery } = alertmanagerApi; - const { currentData: amChoiceStatus } = useGetAlertmanagerChoiceStatusQuery(); - const viewingInternalAM = currentAlertmanager === GRAFANA_RULES_SOURCE_NAME; + const { currentData: amChoiceStatus } = alertmanagerApi.endpoints.getAlertmanagerChoiceStatus.useQuery(undefined, { + skip: !viewingInternalAM, + }); + const interactsWithExternalAMs = amChoiceStatus?.alertmanagersChoice && [AlertmanagerChoice.External, AlertmanagerChoice.All].includes(amChoiceStatus?.alertmanagersChoice); diff --git a/public/app/features/alerting/unified/components/MoreButton.tsx b/public/app/features/alerting/unified/components/MoreButton.tsx new file mode 100644 index 00000000000..a3f824d6505 --- /dev/null +++ b/public/app/features/alerting/unified/components/MoreButton.tsx @@ -0,0 +1,24 @@ +import React, { forwardRef, Ref } from 'react'; + +import { Stack } from '@grafana/experimental'; +import { Button, ButtonProps, Icon } from '@grafana/ui'; + +const MoreButton = forwardRef(function MoreButton(props: ButtonProps, ref: Ref) { + return ( + + ); +}); + +export default MoreButton; diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.test.tsx index 08261d21f91..87ba1ca634c 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.test.tsx @@ -58,6 +58,72 @@ describe('ContactPoints', () => { expect(screen.getByText('grafana-default-email')).toBeInTheDocument(); expect(screen.getAllByTestId('contact-point')).toHaveLength(4); }); + + it('should call delete when clicked and not disabled', async () => { + const onDelete = jest.fn(); + + render(, { + wrapper, + }); + + const moreActions = screen.getByRole('button', { name: 'more-actions' }); + await userEvent.click(moreActions); + + const deleteButton = screen.getByRole('menuitem', { name: /delete/i }); + await userEvent.click(deleteButton); + + expect(onDelete).toHaveBeenCalledWith('my-contact-point'); + }); + + it('should disable edit button', async () => { + render(, { + wrapper, + }); + + const moreActions = screen.getByRole('button', { name: 'more-actions' }); + expect(moreActions).not.toBeDisabled(); + + const editAction = screen.getByTestId('edit-action'); + expect(editAction).toHaveAttribute('aria-disabled', 'true'); + }); + + it('should disable buttons when provisioned', async () => { + render(, { + wrapper, + }); + + expect(screen.getByText(/provisioned/i)).toBeInTheDocument(); + + const editAction = screen.queryByTestId('edit-action'); + expect(editAction).not.toBeInTheDocument(); + + const viewAction = screen.getByRole('link', { name: /view/i }); + expect(viewAction).toBeInTheDocument(); + + const moreActions = screen.getByRole('button', { name: 'more-actions' }); + expect(moreActions).not.toBeDisabled(); + await userEvent.click(moreActions); + + const deleteButton = screen.getByRole('menuitem', { name: /delete/i }); + expect(deleteButton).toBeDisabled(); + }); + + it('should disable delete when contact point is linked to at least one notification policy', async () => { + render( + , + { + wrapper, + } + ); + + expect(screen.getByRole('link', { name: 'is used by 1 notification policy' })).toBeInTheDocument(); + + const moreActions = screen.getByRole('button', { name: 'more-actions' }); + await userEvent.click(moreActions); + + const deleteButton = screen.getByRole('menuitem', { name: /delete/i }); + expect(deleteButton).toBeDisabled(); + }); }); describe('Mimir-flavored alertmanager', () => { @@ -98,71 +164,6 @@ describe('ContactPoints', () => { }); }); -describe('ContactPoint', () => { - it('should call delete when clicked and not disabled', async () => { - const onDelete = jest.fn(); - - render(, { - wrapper, - }); - - const moreActions = screen.getByRole('button', { name: 'more-actions' }); - await userEvent.click(moreActions); - - const deleteButton = screen.getByRole('menuitem', { name: /delete/i }); - await userEvent.click(deleteButton); - - expect(onDelete).toHaveBeenCalledWith('my-contact-point'); - }); - - it('should disable edit button', async () => { - render(, { - wrapper, - }); - - const moreActions = screen.getByRole('button', { name: 'more-actions' }); - expect(moreActions).not.toBeDisabled(); - - const editAction = screen.getByTestId('edit-action'); - expect(editAction).toHaveAttribute('aria-disabled', 'true'); - }); - - it('should disable buttons when provisioned', async () => { - render(, { - wrapper, - }); - - expect(screen.getByText(/provisioned/i)).toBeInTheDocument(); - - const editAction = screen.queryByTestId('edit-action'); - expect(editAction).not.toBeInTheDocument(); - - const viewAction = screen.getByRole('link', { name: /view/i }); - expect(viewAction).toBeInTheDocument(); - - const moreActions = screen.getByRole('button', { name: 'more-actions' }); - expect(moreActions).not.toBeDisabled(); - await userEvent.click(moreActions); - - const deleteButton = screen.getByRole('menuitem', { name: /delete/i }); - expect(deleteButton).toBeDisabled(); - }); - - it('should disable delete when contact point is linked to at least one notification policy', async () => { - render(, { - wrapper, - }); - - expect(screen.getByRole('link', { name: 'is used by 1 notification policy' })).toBeInTheDocument(); - - const moreActions = screen.getByRole('button', { name: 'more-actions' }); - await userEvent.click(moreActions); - - const deleteButton = screen.getByRole('menuitem', { name: /delete/i }); - expect(deleteButton).toBeDisabled(); - }); -}); - const wrapper = ({ children }: PropsWithChildren) => ( {children} diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx index 0576fdffc92..0881c8222a4 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx @@ -1,15 +1,15 @@ import { css } from '@emotion/css'; import { SerializedError } from '@reduxjs/toolkit'; -import { groupBy, size, uniqueId, upperFirst } from 'lodash'; +import { groupBy, size, upperFirst } from 'lodash'; import pluralize from 'pluralize'; -import React, { ReactNode, useState } from 'react'; +import React, { Fragment, ReactNode, useCallback, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; +import { useToggle } from 'react-use'; import { dateTime, GrafanaTheme2 } from '@grafana/data'; import { Stack } from '@grafana/experimental'; import { Alert, - Button, Dropdown, Icon, LoadingPlaceholder, @@ -22,47 +22,65 @@ import { TabContent, Tab, Pagination, + Button, } from '@grafana/ui'; -import { contextSrv } from 'app/core/core'; import ConditionalWrap from 'app/features/alerting/components/ConditionalWrap'; -import { isOrgAdmin } from 'app/features/plugins/admin/permissions'; import { receiverTypeNames } from 'app/plugins/datasource/alertmanager/consts'; import { GrafanaManagedReceiverConfig } from 'app/plugins/datasource/alertmanager/types'; import { GrafanaNotifierType, NotifierStatus } from 'app/types/alerting'; +import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities'; import { usePagination } from '../../hooks/usePagination'; import { useAlertmanager } from '../../state/AlertmanagerContext'; import { INTEGRATION_ICONS } from '../../types/contact-points'; -import { getNotificationsPermissions } from '../../utils/access-control'; -import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource'; +import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { createUrl } from '../../utils/url'; +import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning'; import { MetaText } from '../MetaText'; +import MoreButton from '../MoreButton'; import { ProvisioningBadge } from '../Provisioning'; import { Spacer } from '../Spacer'; import { Strong } from '../Strong'; +import { GrafanaReceiverExporter } from '../export/GrafanaReceiverExporter'; +import { GrafanaReceiversExporter } from '../export/GrafanaReceiversExporter'; import { GlobalConfigAlert } from '../receivers/ReceiversAndTemplatesView'; import { UnusedContactPointBadge } from '../receivers/ReceiversTable'; +import { ReceiverMetadataBadge } from '../receivers/grafanaAppReceivers/ReceiverMetadataBadge'; +import { ReceiverPluginMetadata } from '../receivers/grafanaAppReceivers/useReceiversMetadata'; import { MessageTemplates } from './MessageTemplates'; import { useDeleteContactPointModal } from './Modals'; -import { RECEIVER_STATUS_KEY, useContactPointsWithStatus, useDeleteContactPoint } from './useContactPoints'; -import { ContactPointWithStatus, getReceiverDescription, isProvisioned, ReceiverConfigWithStatus } from './utils'; +import { + RECEIVER_META_KEY, + RECEIVER_PLUGIN_META_KEY, + RECEIVER_STATUS_KEY, + useContactPointsWithStatus, + useDeleteContactPoint, +} from './useContactPoints'; +import { ContactPointWithMetadata, getReceiverDescription, isProvisioned, ReceiverConfigWithMetadata } from './utils'; enum ActiveTab { ContactPoints, MessageTemplates, } -const DEFAULT_PAGE_SIZE = 25; +const DEFAULT_PAGE_SIZE = 10; const ContactPoints = () => { const { selectedAlertmanager } = useAlertmanager(); // TODO hook up to query params const [activeTab, setActiveTab] = useState(ActiveTab.ContactPoints); - let { isLoading, error, contactPoints } = useContactPointsWithStatus(selectedAlertmanager!); + let { isLoading, error, contactPoints } = useContactPointsWithStatus(); const { deleteTrigger, updateAlertmanagerState } = useDeleteContactPoint(selectedAlertmanager!); + const [addContactPointSupported, addContactPointAllowed] = useAlertmanagerAbility( + AlertmanagerAction.CreateContactPoint + ); + const [exportContactPointsSupported, exportContactPointsAllowed] = useAlertmanagerAbility( + AlertmanagerAction.ExportContactPoint + ); const [DeleteModal, showDeleteModal] = useDeleteContactPointModal(deleteTrigger, updateAlertmanagerState.isLoading); + const [ExportDrawer, showExportDrawer] = useExportContactPoint(); const showingContactPoints = activeTab === ActiveTab.ContactPoints; const showingMessageTemplates = activeTab === ActiveTab.MessageTemplates; @@ -73,13 +91,11 @@ const ContactPoints = () => { } const isGrafanaManagedAlertmanager = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME; - const isVanillaAlertmanager = isVanillaPrometheusAlertManagerDataSource(selectedAlertmanager!); - const permissions = getNotificationsPermissions(selectedAlertmanager!); - - const allowedToAddContactPoint = contextSrv.hasPermission(permissions.create); return ( <> + + { active={showingMessageTemplates} onChangeTab={() => setActiveTab(ActiveTab.MessageTemplates)} /> - - {showingContactPoints && ( - - Add contact point - - )} - {showingMessageTemplates && ( - - Add message template - - )} @@ -123,9 +122,34 @@ const ContactPoints = () => { ) : ( <> {/* TODO we can add some additional info here with a ToggleTip */} - - Define where notifications are sent, a contact point can contain multiple integrations. - + + + Define where notifications are sent, a contact point can contain multiple integrations. + + + + {addContactPointSupported && ( + + Add contact point + + )} + {exportContactPointsSupported && ( + + )} + + { {/* Message Templates tab */} {showingMessageTemplates && ( <> - - Create message templates to customize your notifications. - + + + Create message templates to customize your notifications. + + + + Add message template + + )} @@ -152,12 +182,13 @@ const ContactPoints = () => { {DeleteModal} + {ExportDrawer} ); }; interface ContactPointsListProps { - contactPoints: ContactPointWithStatus[]; + contactPoints: ContactPointWithMetadata[]; disabled?: boolean; onDelete: (name: string) => void; pageSize?: number; @@ -198,7 +229,7 @@ interface ContactPointProps { name: string; disabled?: boolean; provisioned?: boolean; - receivers: ReceiverConfigWithStatus[]; + receivers: ReceiverConfigWithMetadata[]; policies?: number; onDelete: (name: string) => void; } @@ -228,16 +259,21 @@ export const ContactPoint = ({ /> {showFullMetadata ? (
- {receivers?.map((receiver) => { + {receivers.map((receiver, index) => { const diagnostics = receiver[RECEIVER_STATUS_KEY]; + const metadata = receiver[RECEIVER_META_KEY]; const sendingResolved = !Boolean(receiver.disableResolveMessage); + const pluginMetadata = receiver[RECEIVER_PLUGIN_META_KEY]; + const key = metadata.name + index; return ( ); @@ -264,15 +300,55 @@ interface ContactPointHeaderProps { const ContactPointHeader = (props: ContactPointHeaderProps) => { const { name, disabled = false, provisioned = false, policies = 0, onDelete } = props; const styles = useStyles2(getStyles); - const { selectedAlertmanager } = useAlertmanager(); - const permissions = getNotificationsPermissions(selectedAlertmanager ?? ''); + + const [exportSupported, exportAllowed] = useAlertmanagerAbility(AlertmanagerAction.ExportContactPoint); + const [editSupported, editAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateContactPoint); + const [deleteSupported, deleteAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateContactPoint); + + const [ExportDrawer, openExportDrawer] = useExportContactPoint(); const isReferencedByPolicies = policies > 0; - const isGranaManagedAlertmanager = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME; + const canEdit = editSupported && editAllowed && !provisioned; + const canDelete = deleteSupported && deleteAllowed && !provisioned && policies === 0; - // we make a distinction here becase for "canExport" we show the menu item, if not we hide it - const canExport = isGranaManagedAlertmanager; - const allowedToExport = contextSrv.hasPermission(permissions.provisioning.read); + const menuActions: JSX.Element[] = []; + + if (exportSupported) { + menuActions.push( + + openExportDrawer(name)} + /> + + + ); + } + + if (deleteSupported) { + menuActions.push( + ( + + {children} + + )} + > + onDelete(name)} + /> + + ); + } return (
@@ -282,115 +358,70 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => { {name} - {isReferencedByPolicies ? ( + {isReferencedByPolicies && ( is used by {policies} {pluralize('notification policy', policies)} - ) : ( - )} {provisioned && } + {!isReferencedByPolicies && } - {provisioned ? 'View' : 'Edit'} + {canEdit ? 'Edit' : 'View'} - {/* TODO probably want to split this off since there's lots of RBAC involved here */} - - {canExport && ( - <> - - - - )} - 0} - wrap={(children) => ( - - {children} - - )} - > - 0} - onClick={() => onDelete(name)} - /> - - - } - > -
); }; interface ContactPointReceiverProps { + name: string; type: GrafanaNotifierType | string; description?: ReactNode; sendingResolved?: boolean; diagnostics?: NotifierStatus; + pluginMetadata?: ReceiverPluginMetadata; } const ContactPointReceiver = (props: ContactPointReceiverProps) => { - const { type, description, diagnostics, sendingResolved = true } = props; + const { name, type, description, diagnostics, pluginMetadata, sendingResolved = true } = props; const styles = useStyles2(getStyles); const iconName = INTEGRATION_ICONS[type]; const hasMetadata = diagnostics !== undefined; - // TODO get the actual name of the type from /ngalert if grafanaManaged AM - const receiverName = receiverTypeNames[type] ?? upperFirst(type); - return (
{iconName && } - - {receiverName} - + {pluginMetadata ? ( + + ) : ( + + {name} + + )} {description && ( @@ -502,6 +533,44 @@ const ContactPointReceiverMetadataRow = ({ diagnostics, sendingResolved }: Conta ); }; +const ALL_CONTACT_POINTS = Symbol('all contact points'); + +type ExportProps = [JSX.Element | null, (receiver: string | typeof ALL_CONTACT_POINTS) => void]; + +const useExportContactPoint = (): ExportProps => { + const [receiverName, setReceiverName] = useState(null); + const [isExportDrawerOpen, toggleShowExportDrawer] = useToggle(false); + const [decryptSecretsSupported, decryptSecretsAllowed] = useAlertmanagerAbility(AlertmanagerAction.DecryptSecrets); + + const canReadSecrets = decryptSecretsSupported && decryptSecretsAllowed; + + const handleClose = useCallback(() => { + setReceiverName(null); + toggleShowExportDrawer(false); + }, [toggleShowExportDrawer]); + + const handleOpen = (receiverName: string | typeof ALL_CONTACT_POINTS) => { + setReceiverName(receiverName); + toggleShowExportDrawer(true); + }; + + const drawer = useMemo(() => { + if (!receiverName || !isExportDrawerOpen) { + return null; + } + + if (receiverName === ALL_CONTACT_POINTS) { + // use this drawer when we want to export all contact points + return ; + } else { + // use this one for exporting a single contact point + return ; + } + }, [canReadSecrets, isExportDrawerOpen, handleClose, receiverName]); + + return [drawer, handleOpen]; +}; + const getStyles = (theme: GrafanaTheme2) => ({ contactPointWrapper: css({ borderRadius: `${theme.shape.radius.default}`, diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/grafanaManagedServer.ts b/public/app/features/alerting/unified/components/contact-points/__mocks__/grafanaManagedServer.ts index ba281758e43..72116c2598f 100644 --- a/public/app/features/alerting/unified/components/contact-points/__mocks__/grafanaManagedServer.ts +++ b/public/app/features/alerting/unified/components/contact-points/__mocks__/grafanaManagedServer.ts @@ -1,9 +1,11 @@ import { rest } from 'msw'; -import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; +import { AlertmanagerChoice, AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; import { ReceiversStateDTO } from 'app/types'; -import { setupMswServer } from '../../../mockApi'; +import { mockApi, setupMswServer } from '../../../mockApi'; +import { mockAlertmanagerChoiceResponse } from '../../../mocks/alertmanagerApi'; +import { grafanaNotifiersMock } from '../../../mocks/grafana-notifiers'; import alertmanagerMock from './alertmanager.config.mock.json'; import receiversMock from './receivers.mock.json'; @@ -19,6 +21,19 @@ export default () => { // this endpoint is only available for the built-in alertmanager rest.get('/api/alertmanager/grafana/config/api/v1/receivers', (_req, res, ctx) => res(ctx.json(receiversMock)) - ) + ), + // this endpoint will respond if the OnCall plugin is installed + rest.get('/api/plugins/grafana-oncall-app/settings', (_req, res, ctx) => res(ctx.status(404))) ); + + // this endpoint is for rendering the "additional AMs to configure" warning + mockAlertmanagerChoiceResponse(server, { + alertmanagersChoice: AlertmanagerChoice.Internal, + numExternalAlertmanagers: 1, + }); + + // mock the endpoint for contact point metadata + mockApi(server).grafanaNotifiers(grafanaNotifiersMock); + + return server; }; diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/mimirFlavoredServer.ts b/public/app/features/alerting/unified/components/contact-points/__mocks__/mimirFlavoredServer.ts index b5d2ec38274..f27d795cf6c 100644 --- a/public/app/features/alerting/unified/components/contact-points/__mocks__/mimirFlavoredServer.ts +++ b/public/app/features/alerting/unified/components/contact-points/__mocks__/mimirFlavoredServer.ts @@ -18,6 +18,8 @@ export default () => { ), rest.get(`/api/datasources/proxy/uid/${MIMIR_DATASOURCE_UID}/api/v1/status/buildinfo`, (_req, res, ctx) => res(ctx.status(404)) - ) + ), + // this endpoint will respond if the OnCall plugin is installed + rest.get('/api/plugins/grafana-oncall-app/settings', (_req, res, ctx) => res(ctx.status(404))) ); }; diff --git a/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap b/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap index e82347ce7e6..5d26e30334e 100644 --- a/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap +++ b/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap @@ -22,6 +22,11 @@ exports[`useContactPoints should return contact points with status 1`] = ` "name": "email", "sendResolved": true, }, + Symbol(receiver_metadata): { + "description": "Sends notifications using Grafana server configured SMTP settings", + "name": "Email", + }, + Symbol(receiver_plugin_metadata): undefined, }, ], "name": "grafana-default-email", @@ -46,6 +51,11 @@ exports[`useContactPoints should return contact points with status 1`] = ` "name": "email", "sendResolved": true, }, + Symbol(receiver_metadata): { + "description": "Sends notifications using Grafana server configured SMTP settings", + "name": "Email", + }, + Symbol(receiver_plugin_metadata): undefined, }, ], "name": "provisioned-contact-point", @@ -69,6 +79,11 @@ exports[`useContactPoints should return contact points with status 1`] = ` "name": "email", "sendResolved": true, }, + Symbol(receiver_metadata): { + "description": "Sends notifications using Grafana server configured SMTP settings", + "name": "Email", + }, + Symbol(receiver_plugin_metadata): undefined, }, ], "name": "lotsa-emails", @@ -93,6 +108,11 @@ exports[`useContactPoints should return contact points with status 1`] = ` "name": "slack", "sendResolved": true, }, + Symbol(receiver_metadata): { + "description": "Sends notifications to Slack", + "name": "Slack", + }, + Symbol(receiver_plugin_metadata): undefined, }, { "disableResolveMessage": false, @@ -111,6 +131,11 @@ exports[`useContactPoints should return contact points with status 1`] = ` "name": "slack", "sendResolved": true, }, + Symbol(receiver_metadata): { + "description": "Sends notifications to Slack", + "name": "Slack", + }, + Symbol(receiver_plugin_metadata): undefined, }, ], "name": "Slack with multiple channels", diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx index c96d03ae9b9..b5c12d7988f 100644 --- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx @@ -1,15 +1,31 @@ import { renderHook, waitFor } from '@testing-library/react'; +import React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; +import { AccessControlAction } from 'app/types'; + +import { grantUserPermissions } from '../../mocks'; +import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; + import setupGrafanaManagedServer from './__mocks__/grafanaManagedServer'; import { useContactPointsWithStatus } from './useContactPoints'; describe('useContactPoints', () => { setupGrafanaManagedServer(); + beforeAll(() => { + grantUserPermissions([AccessControlAction.AlertingNotificationsRead]); + }); + it('should return contact points with status', async () => { - const { result } = renderHook(() => useContactPointsWithStatus('grafana'), { - wrapper: TestProvider, + const { result } = renderHook(() => useContactPointsWithStatus(), { + wrapper: ({ children }) => ( + + + {children} + + + ), }); await waitFor(() => { diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx index 095c586afec..555341a9f76 100644 --- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx +++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx @@ -7,48 +7,81 @@ import { produce } from 'immer'; import { remove } from 'lodash'; import { alertmanagerApi } from '../../api/alertmanagerApi'; -import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; +import { onCallApi } from '../../api/onCallApi'; +import { usePluginBridge } from '../../hooks/usePluginBridge'; +import { useAlertmanager } from '../../state/AlertmanagerContext'; +import { SupportedPlugin } from '../../types/pluginBridges'; -import { enhanceContactPointsWithStatus } from './utils'; +import { enhanceContactPointsWithMetadata } from './utils'; export const RECEIVER_STATUS_KEY = Symbol('receiver_status'); +export const RECEIVER_META_KEY = Symbol('receiver_metadata'); +export const RECEIVER_PLUGIN_META_KEY = Symbol('receiver_plugin_metadata'); + const RECEIVER_STATUS_POLLING_INTERVAL = 10 * 1000; // 10 seconds /** - * This hook will combine data from two endpoints; + * This hook will combine data from several endpoints; * 1. the alertmanager config endpoint where the definition of the receivers are * 2. (if available) the alertmanager receiver status endpoint, currently Grafana Managed only + * 3. (if available) additional metadata about Grafana Managed contact points + * 4. (if available) the OnCall plugin metadata */ -export function useContactPointsWithStatus(selectedAlertmanager: string) { - const isGrafanaManagedAlertmanager = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME; +export function useContactPointsWithStatus() { + const { selectedAlertmanager, isGrafanaAlertmanager } = useAlertmanager(); + const { installed: onCallPluginInstalled = false, loading: onCallPluginStatusLoading } = usePluginBridge( + SupportedPlugin.OnCall + ); // fetch receiver status if we're dealing with a Grafana Managed Alertmanager const fetchContactPointsStatus = alertmanagerApi.endpoints.getContactPointsStatus.useQuery(undefined, { - // TODO these don't seem to work since we've not called setupListeners() refetchOnFocus: true, refetchOnReconnect: true, // re-fetch status every so often for up-to-date information pollingInterval: RECEIVER_STATUS_POLLING_INTERVAL, // skip fetching receiver statuses if not Grafana AM - skip: !isGrafanaManagedAlertmanager, + skip: !isGrafanaAlertmanager, }); + // fetch notifier metadata from the Grafana API if we're using a Grafana AM – this will be used to add additional + // metadata and canonical names to the receiver + const fetchReceiverMetadata = alertmanagerApi.endpoints.grafanaNotifiers.useQuery(undefined, { + skip: !isGrafanaAlertmanager, + }); + + // if the OnCall plugin is installed, fetch its list of integrations so we can match those to the Grafana Managed contact points + const { data: onCallIntegrations, isLoading: onCallPluginIntegrationsLoading } = + onCallApi.endpoints.grafanaOnCallIntegrations.useQuery(undefined, { + skip: !onCallPluginInstalled || !isGrafanaAlertmanager, + }); + // fetch the latest config from the Alertmanager const fetchAlertmanagerConfiguration = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useQuery( - selectedAlertmanager, + selectedAlertmanager!, { refetchOnFocus: true, refetchOnReconnect: true, selectFromResult: (result) => ({ ...result, - contactPoints: result.data ? enhanceContactPointsWithStatus(result.data, fetchContactPointsStatus.data) : [], + contactPoints: result.data + ? enhanceContactPointsWithMetadata( + result.data, + fetchContactPointsStatus.data, + fetchReceiverMetadata.data, + onCallPluginInstalled ? onCallIntegrations ?? [] : null + ) + : [], }), } ); - // TODO kinda yucky to combine hooks like this, better alternative? + // we will fail silently for fetching OnCall plugin status and integrations const error = fetchAlertmanagerConfiguration.error ?? fetchContactPointsStatus.error; - const isLoading = fetchAlertmanagerConfiguration.isLoading || fetchContactPointsStatus.isLoading; + const isLoading = + fetchAlertmanagerConfiguration.isLoading || + fetchContactPointsStatus.isLoading || + onCallPluginStatusLoading || + onCallPluginIntegrationsLoading; const contactPoints = fetchAlertmanagerConfiguration.contactPoints; diff --git a/public/app/features/alerting/unified/components/contact-points/utils.ts b/public/app/features/alerting/unified/components/contact-points/utils.ts index bcec11e595a..414d7bbc00c 100644 --- a/public/app/features/alerting/unified/components/contact-points/utils.ts +++ b/public/app/features/alerting/unified/components/contact-points/utils.ts @@ -1,4 +1,4 @@ -import { countBy, split, trim } from 'lodash'; +import { countBy, split, trim, upperFirst } from 'lodash'; import { ReactNode } from 'react'; import { @@ -7,12 +7,15 @@ import { GrafanaManagedReceiverConfig, Route, } from 'app/plugins/datasource/alertmanager/types'; -import { NotifierStatus, ReceiversStateDTO } from 'app/types'; +import { NotifierDTO, NotifierStatus, ReceiversStateDTO } from 'app/types'; +import { OnCallIntegrationDTO } from '../../api/onCallApi'; import { computeInheritedTree } from '../../utils/notification-policies'; import { extractReceivers } from '../../utils/receivers'; +import { ReceiverTypes } from '../receivers/grafanaAppReceivers/onCall/onCall'; +import { getOnCallMetadata, ReceiverPluginMetadata } from '../receivers/grafanaAppReceivers/useReceiversMetadata'; -import { RECEIVER_STATUS_KEY } from './useContactPoints'; +import { RECEIVER_META_KEY, RECEIVER_PLUGIN_META_KEY, RECEIVER_STATUS_KEY } from './useContactPoints'; export function isProvisioned(contactPoint: GrafanaManagedContactPoint) { // for some reason the provenance is on the receiver and not the entire contact point @@ -22,7 +25,7 @@ export function isProvisioned(contactPoint: GrafanaManagedContactPoint) { } // TODO we should really add some type information to these receiver settings... -export function getReceiverDescription(receiver: GrafanaManagedReceiverConfig): ReactNode | undefined { +export function getReceiverDescription(receiver: ReceiverConfigWithMetadata): ReactNode | undefined { switch (receiver.type) { case 'email': { const hasEmailAddresses = 'addresses' in receiver.settings; // when dealing with alertmanager email_configs we don't normalize the settings @@ -40,8 +43,11 @@ export function getReceiverDescription(receiver: GrafanaManagedReceiverConfig): const url = receiver.settings['url']; return url; } + case ReceiverTypes.OnCall: { + return receiver[RECEIVER_PLUGIN_META_KEY]?.description; + } default: - return undefined; + return receiver[RECEIVER_META_KEY]?.description; } } @@ -64,15 +70,21 @@ function summarizeEmailAddresses(addresses: string): string { } // Grafana Managed contact points have receivers with additional diagnostics -export interface ReceiverConfigWithStatus extends GrafanaManagedReceiverConfig { +export interface ReceiverConfigWithMetadata extends GrafanaManagedReceiverConfig { // we're using a symbol here so we'll never have a conflict on keys for a receiver // we also specify that the diagnostics might be "undefined" for vanilla Alertmanager [RECEIVER_STATUS_KEY]?: NotifierStatus | undefined; + [RECEIVER_META_KEY]: { + name: string; + description?: string; + }; + // optional metadata that comes from a particular plugin (like Grafana OnCall) + [RECEIVER_PLUGIN_META_KEY]?: ReceiverPluginMetadata; } -export interface ContactPointWithStatus extends GrafanaManagedContactPoint { +export interface ContactPointWithMetadata extends GrafanaManagedContactPoint { numberOfPolicies: number; - grafana_managed_receiver_configs: ReceiverConfigWithStatus[]; + grafana_managed_receiver_configs: ReceiverConfigWithMetadata[]; } /** @@ -80,10 +92,12 @@ export interface ContactPointWithStatus extends GrafanaManagedContactPoint { * 1. we iterate over all contact points * 2. for each contact point we "enhance" it with the status or "undefined" for vanilla Alertmanager */ -export function enhanceContactPointsWithStatus( +export function enhanceContactPointsWithMetadata( result: AlertManagerCortexConfig, - status: ReceiversStateDTO[] = [] -): ContactPointWithStatus[] { + status: ReceiversStateDTO[] = [], + notifiers: NotifierDTO[] = [], + onCallIntegrations: OnCallIntegrationDTO[] | null +): ContactPointWithMetadata[] { const contactPoints = result.alertmanager_config.receivers ?? []; // compute the entire inherited tree before finding what notification policies are using a particular contact point @@ -98,10 +112,17 @@ export function enhanceContactPointsWithStatus( return { ...contactPoint, numberOfPolicies: usedContactPointsByName[contactPoint.name] ?? 0, - grafana_managed_receiver_configs: receivers.map((receiver, index) => ({ - ...receiver, - [RECEIVER_STATUS_KEY]: statusForReceiver?.integrations[index], - })), + grafana_managed_receiver_configs: receivers.map((receiver, index) => { + const isOnCallReceiver = receiver.type === ReceiverTypes.OnCall; + + return { + ...receiver, + [RECEIVER_STATUS_KEY]: statusForReceiver?.integrations[index], + [RECEIVER_META_KEY]: getNotifierMetadata(notifiers, receiver), + // if OnCall plugin is installed, we'll add it to the receiver's plugin metadata + [RECEIVER_PLUGIN_META_KEY]: isOnCallReceiver ? getOnCallMetadata(onCallIntegrations, receiver) : undefined, + }; + }), }; }); } @@ -114,3 +135,12 @@ export function getUsedContactPoints(route: Route): string[] { return childrenContactPoints; } + +function getNotifierMetadata(notifiers: NotifierDTO[], receiver: GrafanaManagedReceiverConfig) { + const match = notifiers.find((notifier) => notifier.type === receiver.type); + + return { + name: match?.name ?? upperFirst(receiver.type), + description: match?.description, + }; +} diff --git a/public/app/features/alerting/unified/components/export/FileExportPreview.tsx b/public/app/features/alerting/unified/components/export/FileExportPreview.tsx index f9729cc4a3a..2a7f5c06ee6 100644 --- a/public/app/features/alerting/unified/components/export/FileExportPreview.tsx +++ b/public/app/features/alerting/unified/components/export/FileExportPreview.tsx @@ -25,9 +25,7 @@ export function FileExportPreview({ format, textDefinition, downloadFileName, on type: `application/${format};charset=utf-8`, }); saveAs(blob, `${downloadFileName}.${format}`); - - onClose(); - }, [textDefinition, downloadFileName, format, onClose]); + }, [textDefinition, downloadFileName, format]); const formattedTextDefinition = useMemo(() => { const provider = allGrafanaExportProviders[format]; @@ -49,6 +47,7 @@ export function FileExportPreview({ format, textDefinition, downloadFileName, on minimap: { enabled: false, }, + scrollBeyondLastLine: false, lineNumbers: 'on', readOnly: true, }} diff --git a/public/app/features/alerting/unified/components/notification-policies/ContactPointSelector.tsx b/public/app/features/alerting/unified/components/notification-policies/ContactPointSelector.tsx new file mode 100644 index 00000000000..0a543be22b3 --- /dev/null +++ b/public/app/features/alerting/unified/components/notification-policies/ContactPointSelector.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import { SelectableValue } from '@grafana/data'; +import { Stack } from '@grafana/experimental'; +import { Select, SelectCommonProps, Text } from '@grafana/ui'; + +import { + RECEIVER_META_KEY, + RECEIVER_PLUGIN_META_KEY, + useContactPointsWithStatus, +} from '../contact-points/useContactPoints'; +import { ReceiverConfigWithMetadata } from '../contact-points/utils'; + +export const ContactPointSelector = (props: SelectCommonProps) => { + const { contactPoints, isLoading, error } = useContactPointsWithStatus(); + + // TODO error handling + if (error) { + return Failed to load contact points; + } + + const options: Array> = contactPoints.map((contactPoint) => { + return { + label: contactPoint.name, + value: contactPoint.name, + component: () => , + }; + }); + + return + + ); +} diff --git a/public/app/features/explore/Logs/LogsTable.test.tsx b/public/app/features/explore/Logs/LogsTable.test.tsx index dc8665e810d..9f4c108bdd4 100644 --- a/public/app/features/explore/Logs/LogsTable.test.tsx +++ b/public/app/features/explore/Logs/LogsTable.test.tsx @@ -7,6 +7,7 @@ import { config } from '@grafana/runtime'; import { extractFieldsTransformer } from 'app/features/transformers/extractFields/extractFields'; import { LogsTable } from './LogsTable'; +import { getMockElasticFrame, getMockLokiFrame, getMockLokiFrameDataPlane } from './utils/testMocks.test'; jest.mock('@grafana/runtime', () => { const actual = jest.requireActual('@grafana/runtime'); @@ -18,6 +19,68 @@ jest.mock('@grafana/runtime', () => { }; }); +const getComponent = (partialProps?: Partial>, logs?: DataFrame) => { + const testDataFrame = { + fields: [ + { + config: {}, + name: 'Time', + type: FieldType.time, + values: ['2019-01-01 10:00:00', '2019-01-01 11:00:00', '2019-01-01 12:00:00'], + }, + { + config: {}, + name: 'line', + type: FieldType.string, + values: ['log message 1', 'log message 2', 'log message 3'], + }, + { + config: {}, + name: 'tsNs', + type: FieldType.string, + values: ['ts1', 'ts2', 'ts3'], + }, + { + config: {}, + name: 'labels', + type: FieldType.other, + typeInfo: { + frame: 'json.RawMessage', + }, + values: [{ foo: 'bar' }, { foo: 'bar' }, { foo: 'bar' }], + }, + ], + length: 3, + }; + return ( + undefined} + timeZone={'utc'} + width={50} + range={{ + from: toUtc('2019-01-01 10:00:00'), + to: toUtc('2019-01-01 16:00:00'), + raw: { from: 'now-1h', to: 'now' }, + }} + logsFrames={[logs ?? testDataFrame]} + {...partialProps} + /> + ); +}; +const setup = (partialProps?: Partial>, logs?: DataFrame) => { + return render( + getComponent( + { + ...partialProps, + }, + logs + ) + ); +}; + describe('LogsTable', () => { beforeAll(() => { const transformers = [extractFieldsTransformer, organizeFieldsTransformer]; @@ -35,59 +98,6 @@ describe('LogsTable', () => { }); }); - const getComponent = (partialProps?: Partial>, logs?: DataFrame) => { - const testDataFrame = { - fields: [ - { - config: {}, - name: 'Time', - type: FieldType.time, - values: ['2019-01-01 10:00:00', '2019-01-01 11:00:00', '2019-01-01 12:00:00'], - }, - { - config: {}, - name: 'line', - type: FieldType.string, - values: ['log message 1', 'log message 2', 'log message 3'], - }, - { - config: {}, - name: 'tsNs', - type: FieldType.string, - values: ['ts1', 'ts2', 'ts3'], - }, - { - config: {}, - name: 'labels', - type: FieldType.other, - typeInfo: { - frame: 'json.RawMessage', - }, - values: ['{"foo":"bar"}', '{"foo":"bar"}', '{"foo":"bar"}'], - }, - ], - length: 3, - }; - return ( - undefined} - timeZone={'utc'} - width={50} - range={{ - from: toUtc('2019-01-01 10:00:00'), - to: toUtc('2019-01-01 16:00:00'), - raw: { from: 'now-1h', to: 'now' }, - }} - logsFrames={[logs ?? testDataFrame]} - {...partialProps} - /> - ); - }; - const setup = (partialProps?: Partial>, logs?: DataFrame) => { - return render(getComponent(partialProps, logs)); - }; - let originalVisualisationTypeValue = config.featureToggles.logsExploreTableVisualisation; beforeAll(() => { @@ -109,18 +119,26 @@ describe('LogsTable', () => { }); }); - it('should render 4 table rows', async () => { - setup(); + it('should render extracted labels as columns (elastic)', async () => { + setup({ + logsFrames: [getMockElasticFrame()], + }); await waitFor(() => { - const rows = screen.getAllByRole('row'); - // tableFrame has 3 rows + 1 header row - expect(rows.length).toBe(4); + const columns = screen.getAllByRole('columnheader'); + expect(columns[0].textContent).toContain('@timestamp'); + expect(columns[1].textContent).toContain('line'); + expect(columns[2].textContent).toContain('counter'); + expect(columns[3].textContent).toContain('level'); }); }); - it('should render extracted labels as columns', async () => { - setup(); + it('should render extracted labels as columns (loki)', async () => { + setup({ + columnsWithMeta: { + foo: { active: true, percentOfLinesWithLabel: 3 }, + }, + }); await waitFor(() => { const columns = screen.getAllByRole('columnheader'); @@ -132,7 +150,7 @@ describe('LogsTable', () => { }); it('should not render `tsNs`', async () => { - setup(); + setup(undefined, getMockLokiFrame()); await waitFor(() => { const columns = screen.queryAllByRole('columnheader', { name: 'tsNs' }); @@ -141,47 +159,86 @@ describe('LogsTable', () => { }); }); - it('should render a datalink for each row', async () => { - render( - getComponent( - {}, - { - fields: [ - { - config: {}, - name: 'Time', - type: FieldType.time, - values: ['2019-01-01 10:00:00', '2019-01-01 11:00:00', '2019-01-01 12:00:00'], - }, - { - config: {}, - name: 'line', - type: FieldType.string, - values: ['log message 1', 'log message 2', 'log message 3'], - }, - { - config: { - links: [ - { - url: 'http://example.com', - title: 'foo', - }, - ], - }, - name: 'link', - type: FieldType.string, - values: ['ts1', 'ts2', 'ts3'], - }, - ], - length: 3, - } - ) - ); + it('should not render `labels`', async () => { + setup(); await waitFor(() => { - const links = screen.getAllByRole('link'); + const columns = screen.queryAllByRole('columnheader', { name: 'labels' }); - expect(links.length).toBe(3); + expect(columns.length).toBe(0); + }); + }); + + describe('LogsTable (loki dataplane)', () => { + let originalVisualisationTypeValue = config.featureToggles.logsExploreTableVisualisation; + let originalLokiDataplaneValue = config.featureToggles.lokiLogsDataplane; + + beforeAll(() => { + originalVisualisationTypeValue = config.featureToggles.logsExploreTableVisualisation; + originalLokiDataplaneValue = config.featureToggles.lokiLogsDataplane; + config.featureToggles.logsExploreTableVisualisation = true; + config.featureToggles.lokiLogsDataplane = true; + }); + + afterAll(() => { + config.featureToggles.logsExploreTableVisualisation = originalVisualisationTypeValue; + config.featureToggles.lokiLogsDataplane = originalLokiDataplaneValue; + }); + + it('should render 4 table rows', async () => { + setup(undefined, getMockLokiFrameDataPlane()); + + await waitFor(() => { + const rows = screen.getAllByRole('row'); + // tableFrame has 3 rows + 1 header row + expect(rows.length).toBe(4); + }); + }); + + it('should render a datalink for each row', async () => { + render(getComponent({}, getMockLokiFrameDataPlane())); + + await waitFor(() => { + const links = screen.getAllByRole('link'); + + expect(links.length).toBe(3); + }); + }); + + it('should not render `attributes`', async () => { + setup(undefined, getMockLokiFrameDataPlane()); + + await waitFor(() => { + const columns = screen.queryAllByRole('columnheader', { name: 'attributes' }); + + expect(columns.length).toBe(0); + }); + }); + + it('should not render `tsNs`', async () => { + setup(undefined, getMockLokiFrameDataPlane()); + + await waitFor(() => { + const columns = screen.queryAllByRole('columnheader', { name: 'tsNs' }); + + expect(columns.length).toBe(0); + }); + }); + + it('should render extracted labels as columns (loki dataplane)', async () => { + setup({ + columnsWithMeta: { + foo: { active: true, percentOfLinesWithLabel: 3 }, + }, + }); + + await waitFor(() => { + const columns = screen.getAllByRole('columnheader'); + + expect(columns[0].textContent).toContain('Time'); + expect(columns[1].textContent).toContain('line'); + expect(columns[2].textContent).toContain('foo'); + }); }); }); }); diff --git a/public/app/features/explore/Logs/LogsTable.tsx b/public/app/features/explore/Logs/LogsTable.tsx index ebcb13dfc0a..f53d10d3c2a 100644 --- a/public/app/features/explore/Logs/LogsTable.tsx +++ b/public/app/features/explore/Logs/LogsTable.tsx @@ -1,11 +1,14 @@ -import memoizeOne from 'memoize-one'; import React, { useCallback, useEffect, useState } from 'react'; import { lastValueFrom } from 'rxjs'; import { applyFieldOverrides, + CustomTransformOperator, DataFrame, + DataFrameType, + DataTransformerConfig, Field, + FieldType, LogsSortOrder, sortDataFrame, SplitOpen, @@ -14,38 +17,41 @@ import { ValueLinkConfig, } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { Table } from '@grafana/ui'; +import { AdHocFilterItem, Table } from '@grafana/ui'; +import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR } from '@grafana/ui/src/components/Table/types'; import { separateVisibleFields } from 'app/features/logs/components/logParser'; -import { parseLogsFrame } from 'app/features/logs/logsFrame'; +import { LogsFrame, parseLogsFrame } from 'app/features/logs/logsFrame'; import { getFieldLinksForExplore } from '../utils/links'; +import { fieldNameMeta } from './LogsTableWrap'; + interface Props { - logsFrames?: DataFrame[]; + logsFrames: DataFrame[]; width: number; timeZone: string; splitOpen: SplitOpen; range: TimeRange; logsSortOrder: LogsSortOrder; + columnsWithMeta: Record; + height: number; + onClickFilterLabel?: (key: string, value: string, refId?: string) => void; + onClickFilterOutLabel?: (key: string, value: string, refId?: string) => void; } -const getTableHeight = memoizeOne((dataFrames: DataFrame[] | undefined) => { - const largestFrameLength = dataFrames?.reduce((length, frame) => { - return frame.length > length ? frame.length : length; - }, 0); - // from TableContainer.tsx - return Math.min(600, Math.max(largestFrameLength ?? 0 * 36, 300) + 40 + 46); -}); - -export const LogsTable: React.FunctionComponent = (props) => { - const { timeZone, splitOpen, range, logsSortOrder, width, logsFrames } = props; - +export function LogsTable(props: Props) { + const { timeZone, splitOpen, range, logsSortOrder, width, logsFrames, columnsWithMeta } = props; const [tableFrame, setTableFrame] = useState(undefined); + // Only a single frame (query) is supported currently + const logFrameRaw = logsFrames ? logsFrames[0] : undefined; + const prepareTableFrame = useCallback( (frame: DataFrame): DataFrame => { + // Parse the dataframe to a logFrame const logsFrame = parseLogsFrame(frame); const timeIndex = logsFrame?.timeField.index; + const sortedFrame = sortDataFrame(frame, timeIndex, logsSortOrder === LogsSortOrder.Descending); const [frameWithOverrides] = applyFieldOverrides({ @@ -74,93 +80,203 @@ export const LogsTable: React.FunctionComponent = (props) => { field.config = { ...field.config, custom: { - filterable: true, inspect: true, + filterable: true, // This sets the columns to be filterable ...field.config.custom, }, + // This sets the individual field value as filterable + filterable: isFieldFilterable(field, logsFrame ?? undefined), }; } return frameWithOverrides; }, - [logsSortOrder, range, splitOpen, timeZone] + [logsSortOrder, timeZone, splitOpen, range] ); useEffect(() => { const prepare = async () => { - if (!logsFrames || !logsFrames.length) { + // Parse the dataframe to a logFrame + const logsFrame = logFrameRaw ? parseLogsFrame(logFrameRaw) : undefined; + + if (!logFrameRaw || !logsFrame) { setTableFrame(undefined); return; } - // TODO: This does not work with multiple logs queries for now, as we currently only support one logs frame. - let dataFrame = logsFrames[0]; - const logsFrame = parseLogsFrame(dataFrame); - const timeIndex = logsFrame?.timeField.index; - dataFrame = sortDataFrame(dataFrame, timeIndex, logsSortOrder === LogsSortOrder.Descending); + let dataFrame = logFrameRaw; // create extract JSON transformation for every field that is `json.RawMessage` - // TODO: explore if `logsFrame.ts` can help us with getting the right fields - const transformations = dataFrame.fields - .filter((field: Field & { typeInfo?: { frame: string } }) => { - return field.typeInfo?.frame === 'json.RawMessage'; - }) - .flatMap((field: Field) => { - return [ - { - id: 'extractFields', - options: { - format: 'json', - keepTime: false, - replace: false, - source: field.name, - }, - }, - // hide the field that was extracted - { - id: 'organize', - options: { - excludeByName: { - [field.name]: true, - }, - }, - }, - ]; - }); + const transformations: Array = + extractFieldsAndExclude(dataFrame); - // remove fields that should not be displayed + // remove hidden fields + transformations.push(...removeHiddenFields(dataFrame)); + let labelFilters = buildLabelFilters(columnsWithMeta, logsFrame); - const hiddenFields = separateVisibleFields(dataFrame, { keepBody: true, keepTimestamp: true }).hidden; - hiddenFields.forEach((field: Field, index: number) => { - transformations.push({ + // Add the label filters to the transformations + const transform = getLabelFiltersTransform(labelFilters); + if (transform) { + transformations.push(transform); + } + + if (transformations.length > 0) { + const transformedDataFrame = await lastValueFrom(transformDataFrame(transformations, [dataFrame])); + const tableFrame = prepareTableFrame(transformedDataFrame[0]); + setTableFrame(tableFrame); + } else { + setTableFrame(prepareTableFrame(dataFrame)); + } + }; + prepare(); + }, [columnsWithMeta, logFrameRaw, logsSortOrder, prepareTableFrame]); + + if (!tableFrame) { + return null; + } + + const onCellFilterAdded = (filter: AdHocFilterItem) => { + const { value, key, operator } = filter; + const { onClickFilterLabel, onClickFilterOutLabel } = props; + if (!onClickFilterLabel || !onClickFilterOutLabel) { + return; + } + if (operator === FILTER_FOR_OPERATOR) { + onClickFilterLabel(key, value); + } + + if (operator === FILTER_OUT_OPERATOR) { + onClickFilterOutLabel(key, value); + } + }; + + return ( + + ); +} + +const isFieldFilterable = (field: Field, logsFrame?: LogsFrame | undefined) => { + if (!logsFrame) { + return false; + } + if (logsFrame.bodyField.name === field.name) { + return false; + } + if (logsFrame.timeField.name === field.name) { + return false; + } + // @todo not currently excluding derived fields from filtering + + return true; +}; + +// TODO: explore if `logsFrame.ts` can help us with getting the right fields +// TODO Why is typeInfo not defined on the Field interface? +function extractFieldsAndExclude(dataFrame: DataFrame) { + return dataFrame.fields + .filter((field: Field & { typeInfo?: { frame: string } }) => { + const isFieldLokiLabels = field.typeInfo?.frame === 'json.RawMessage' && field.name === 'labels'; + const isFieldDataplaneLabels = + field.name === 'attributes' && + field.type === FieldType.other && + dataFrame?.meta?.type === DataFrameType.LogLines; + + return isFieldLokiLabels || isFieldDataplaneLabels; + }) + .flatMap((field: Field) => { + return [ + { + id: 'extractFields', + options: { + format: 'json', + keepTime: false, + replace: false, + source: field.name, + }, + }, + // hide the field that was extracted + { id: 'organize', options: { excludeByName: { [field.name]: true, }, }, - }); - }); - if (transformations.length > 0) { - const [transformedDataFrame] = await lastValueFrom(transformDataFrame(transformations, [dataFrame])); - setTableFrame(prepareTableFrame(transformedDataFrame)); - } else { - setTableFrame(prepareTableFrame(dataFrame)); - } + }, + ]; + }); +} + +function removeHiddenFields(dataFrame: DataFrame): Array { + const transformations: Array = []; + const hiddenFields = separateVisibleFields(dataFrame, { keepBody: true, keepTimestamp: true }).hidden; + hiddenFields.forEach((field: Field) => { + transformations.push({ + id: 'organize', + options: { + excludeByName: { + [field.name]: true, + }, + }, + }); + }); + + return transformations; +} + +function buildLabelFilters(columnsWithMeta: Record, logsFrame: LogsFrame) { + // Create object of label filters to filter out any columns not selected by the user + let labelFilters: Record = {}; + Object.keys(columnsWithMeta) + .filter((key) => !columnsWithMeta[key].active) + .forEach((key) => { + labelFilters[key] = true; + }); + + // We could be getting fresh data + const uniqueLabels = new Set(); + const logFrameLabels = logsFrame?.getAttributesAsLabels(); + + // Populate the set with all labels from latest dataframe + logFrameLabels?.forEach((labels) => { + Object.keys(labels).forEach((label) => { + uniqueLabels.add(label); + }); + }); + + // Check if there are labels in the data, that aren't yet in the labelFilters, and set them to be hidden by the transform + Object.keys(labelFilters).forEach((label) => { + if (!uniqueLabels.has(label)) { + labelFilters[label] = true; + } + }); + + // Check if there are labels in the label filters that aren't yet in the data, and set those to also be hidden + // The next time the column filters are synced any extras will be removed + Array.from(uniqueLabels).forEach((label) => { + if (label in columnsWithMeta && !columnsWithMeta[label]?.active) { + labelFilters[label] = true; + } else if (!labelFilters[label] && !(label in columnsWithMeta)) { + labelFilters[label] = true; + } + }); + return labelFilters; +} + +function getLabelFiltersTransform(labelFilters: Record) { + if (Object.keys(labelFilters).length > 0) { + return { + id: 'organize', + options: { + excludeByName: labelFilters, + }, }; - prepare(); - }, [prepareTableFrame, logsFrames, logsSortOrder]); - - if (!tableFrame) { - return null; } - - return ( -
- ); -}; + return null; +} diff --git a/public/app/features/explore/Logs/LogsTableMultiSelect.tsx b/public/app/features/explore/Logs/LogsTableMultiSelect.tsx new file mode 100644 index 00000000000..92e0f377280 --- /dev/null +++ b/public/app/features/explore/Logs/LogsTableMultiSelect.tsx @@ -0,0 +1,53 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data/src'; +import { useTheme2 } from '@grafana/ui/src'; + +import { LogsTableNavColumn } from './LogsTableNavColumn'; +import { fieldNameMeta } from './LogsTableWrap'; + +function getStyles(theme: GrafanaTheme2) { + return { + sidebarWrap: css({ + overflowY: 'scroll', + height: 'calc(100% - 50px)', + }), + columnHeader: css({ + fontSize: theme.typography.h6.fontSize, + background: theme.colors.background.secondary, + position: 'sticky', + top: 0, + left: 0, + paddingTop: theme.spacing(0.75), + paddingRight: theme.spacing(0.75), + paddingBottom: theme.spacing(0.75), + paddingLeft: theme.spacing(1.5), + zIndex: 3, + marginBottom: theme.spacing(2), + }), + }; +} + +export const LogsTableMultiSelect = (props: { + toggleColumn: (columnName: string) => void; + filteredColumnsWithMeta: Record | undefined; + columnsWithMeta: Record; +}) => { + const theme = useTheme2(); + const styles = getStyles(theme); + + return ( +
+ {/* Sidebar columns */} + <> +
Fields
+ !!value} + /> + +
+ ); +}; diff --git a/public/app/features/explore/Logs/LogsTableNavColumn.tsx b/public/app/features/explore/Logs/LogsTableNavColumn.tsx new file mode 100644 index 00000000000..3540f54f8a9 --- /dev/null +++ b/public/app/features/explore/Logs/LogsTableNavColumn.tsx @@ -0,0 +1,64 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data/src'; +import { Checkbox, useTheme2 } from '@grafana/ui/src'; + +import { fieldNameMeta } from './LogsTableWrap'; + +function getStyles(theme: GrafanaTheme2) { + return { + labelCount: css({ + marginLeft: theme.spacing(0.5), + marginRight: theme.spacing(0.5), + }), + wrap: css({ + display: 'flex', + alignItems: 'center', + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1), + justifyContent: 'space-between', + }), + checkbox: css({}), + columnWrapper: css({ + marginBottom: theme.spacing(1.5), + // need some space or the outline of the checkbox is cut off + paddingLeft: theme.spacing(0.5), + }), + empty: css({ + marginBottom: theme.spacing(2), + marginLeft: theme.spacing(1.75), + fontSize: theme.typography.fontSize, + }), + }; +} + +export const LogsTableNavColumn = (props: { + labels: Record; + valueFilter: (value: number) => boolean; + toggleColumn: (columnName: string) => void; +}): JSX.Element => { + const { labels, valueFilter, toggleColumn } = props; + const theme = useTheme2(); + const styles = getStyles(theme); + const labelKeys = Object.keys(labels).filter((labelName) => valueFilter(labels[labelName].percentOfLinesWithLabel)); + if (labelKeys.length) { + return ( +
+ {labelKeys.map((labelName) => ( +
+ toggleColumn(labelName)} + checked={labels[labelName]?.active ?? false} + /> + ({labels[labelName]?.percentOfLinesWithLabel}%) +
+ ))} +
+ ); + } + + return
No fields
; +}; diff --git a/public/app/features/explore/Logs/LogsTableWrap.test.tsx b/public/app/features/explore/Logs/LogsTableWrap.test.tsx new file mode 100644 index 00000000000..0d6ebdf5fb0 --- /dev/null +++ b/public/app/features/explore/Logs/LogsTableWrap.test.tsx @@ -0,0 +1,165 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import React, { ComponentProps } from 'react'; + +import { + createTheme, + ExploreLogsPanelState, + LogsSortOrder, + standardTransformersRegistry, + toUtc, +} from '@grafana/data/src'; +import { organizeFieldsTransformer } from '@grafana/data/src/transformations/transformers/organize'; +import { config } from '@grafana/runtime'; + +import { extractFieldsTransformer } from '../../transformers/extractFields/extractFields'; + +import { LogsTableWrap } from './LogsTableWrap'; +import { getMockLokiFrame, getMockLokiFrameDataPlane } from './utils/testMocks.test'; + +const getComponent = (partialProps?: Partial>) => { + return ( + undefined} + onClickFilterLabel={() => undefined} + updatePanelState={() => undefined} + panelState={undefined} + logsSortOrder={LogsSortOrder.Descending} + splitOpen={() => undefined} + timeZone={'utc'} + width={50} + logsFrames={[getMockLokiFrame()]} + theme={createTheme()} + {...partialProps} + /> + ); +}; +const setup = (partialProps?: Partial>) => { + return render(getComponent(partialProps)); +}; + +describe('LogsTableWrap', () => { + beforeAll(() => { + const transformers = [extractFieldsTransformer, organizeFieldsTransformer]; + standardTransformersRegistry.setInit(() => { + return transformers.map((t) => { + return { + id: t.id, + aliasIds: t.aliasIds, + name: t.name, + transformation: t, + description: t.description, + editor: () => null, + }; + }); + }); + }); + + it('should render 4 table rows', async () => { + setup(); + + await waitFor(() => { + const rows = screen.getAllByRole('row'); + // tableFrame has 3 rows + 1 header row + expect(rows.length).toBe(4); + }); + }); + + it('should render 4 table rows (dataplane)', async () => { + config.featureToggles.lokiLogsDataplane = true; + setup({ logsFrames: [getMockLokiFrameDataPlane()] }); + + await waitFor(() => { + const rows = screen.getAllByRole('row'); + // tableFrame has 3 rows + 1 header row + expect(rows.length).toBe(4); + }); + }); + + it('updatePanelState should be called when a column is selected', async () => { + const updatePanelState = jest.fn() as (panelState: Partial) => void; + setup({ + panelState: { + visualisationType: 'table', + columns: undefined, + }, + updatePanelState: updatePanelState, + }); + + expect.assertions(3); + + const checkboxLabel = screen.getByLabelText('app'); + expect(checkboxLabel).toBeInTheDocument(); + + // Add a new column + await waitFor(() => { + checkboxLabel.click(); + expect(updatePanelState).toBeCalledWith({ + visualisationType: 'table', + columns: { 0: 'app' }, + }); + }); + + // Remove the same column + await waitFor(() => { + checkboxLabel.click(); + expect(updatePanelState).toBeCalledWith({ + visualisationType: 'table', + columns: {}, + }); + }); + }); + + it('search input should search matching columns', async () => { + config.featureToggles.lokiLogsDataplane = false; + const updatePanelState = jest.fn() as (panelState: Partial) => void; + setup({ + panelState: { + visualisationType: 'table', + columns: undefined, + }, + updatePanelState: updatePanelState, + }); + + await waitFor(() => { + expect(screen.getByLabelText('app')).toBeInTheDocument(); + expect(screen.getByLabelText('cluster')).toBeInTheDocument(); + }); + + const searchInput = screen.getByPlaceholderText('Search fields by name'); + fireEvent.change(searchInput, { target: { value: 'app' } }); + + expect(screen.getByLabelText('app')).toBeInTheDocument(); + await waitFor(() => { + expect(screen.queryByLabelText('cluster')).not.toBeInTheDocument(); + }); + }); + + it('search input should search matching columns (dataplane)', async () => { + config.featureToggles.lokiLogsDataplane = true; + + const updatePanelState = jest.fn() as (panelState: Partial) => void; + setup({ + panelState: {}, + updatePanelState: updatePanelState, + logsFrames: [getMockLokiFrameDataPlane()], + }); + + await waitFor(() => { + expect(screen.getByLabelText('app')).toBeInTheDocument(); + expect(screen.getByLabelText('cluster')).toBeInTheDocument(); + }); + + const searchInput = screen.getByPlaceholderText('Search fields by name'); + fireEvent.change(searchInput, { target: { value: 'app' } }); + + expect(screen.getByLabelText('app')).toBeInTheDocument(); + await waitFor(() => { + expect(screen.queryByLabelText('cluster')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/public/app/features/explore/Logs/LogsTableWrap.tsx b/public/app/features/explore/Logs/LogsTableWrap.tsx new file mode 100644 index 00000000000..08a662a5a02 --- /dev/null +++ b/public/app/features/explore/Logs/LogsTableWrap.tsx @@ -0,0 +1,339 @@ +import { css } from '@emotion/css'; +import { debounce } from 'lodash'; +import React, { useState, useEffect, useCallback } from 'react'; + +import { + DataFrame, + ExploreLogsPanelState, + GrafanaTheme2, + Labels, + LogsSortOrder, + SplitOpen, + TimeRange, +} from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime/src'; +import { Themeable2 } from '@grafana/ui/'; + +import { parseLogsFrame } from '../../logs/logsFrame'; + +import { LogsColumnSearch } from './LogsColumnSearch'; +import { LogsTable } from './LogsTable'; +import { LogsTableMultiSelect } from './LogsTableMultiSelect'; +import { fuzzySearch } from './utils/uFuzzy'; + +interface Props extends Themeable2 { + logsFrames: DataFrame[]; + width: number; + timeZone: string; + splitOpen: SplitOpen; + range: TimeRange; + logsSortOrder: LogsSortOrder; + panelState: ExploreLogsPanelState | undefined; + updatePanelState: (panelState: Partial) => void; + onClickFilterLabel?: (key: string, value: string, refId?: string) => void; + onClickFilterOutLabel?: (key: string, value: string, refId?: string) => void; +} + +export type fieldNameMeta = { percentOfLinesWithLabel: number; active: boolean | undefined }; +type fieldName = string; +type fieldNameMetaStore = Record; + +export function LogsTableWrap(props: Props) { + const { logsFrames } = props; + // Save the normalized cardinality of each label + const [columnsWithMeta, setColumnsWithMeta] = useState(undefined); + + // Filtered copy of columnsWithMeta that only includes matching results + const [filteredColumnsWithMeta, setFilteredColumnsWithMeta] = useState(undefined); + + const [height, setHeight] = useState(600); + + const dataFrame = logsFrames[0]; + + const getColumnsFromProps = useCallback( + (fieldNames: fieldNameMetaStore) => { + const previouslySelected = props.panelState?.columns; + if (previouslySelected) { + Object.values(previouslySelected).forEach((key) => { + if (fieldNames[key]) { + fieldNames[key].active = true; + } + }); + } + return fieldNames; + }, + [props.panelState?.columns] + ); + + /** + * Keeps the filteredColumnsWithMeta state in sync with the columnsWithMeta state, + * which can be updated by explore browser history state changes + * This prevents an edge case bug where the user is navigating while a search is open. + */ + useEffect(() => { + if (!columnsWithMeta || !filteredColumnsWithMeta) { + return; + } + let newFiltered = { ...filteredColumnsWithMeta }; + let flag = false; + Object.keys(columnsWithMeta).forEach((key) => { + if (newFiltered[key] && newFiltered[key].active !== columnsWithMeta[key].active) { + newFiltered[key] = columnsWithMeta[key]; + flag = true; + } + }); + if (flag) { + setFilteredColumnsWithMeta(newFiltered); + } + }, [columnsWithMeta, filteredColumnsWithMeta]); + + /** + * when the query results change, we need to update the columnsWithMeta state + * and reset any local search state + * + * This will also find all the unique labels, and calculate how many log lines have each label into the labelCardinality Map + * Then it normalizes the counts + * + */ + useEffect(() => { + const numberOfLogLines = dataFrame ? dataFrame.length : 0; + const logsFrame = parseLogsFrame(dataFrame); + const labels = logsFrame?.getAttributesAsLabels(); + + const otherFields = logsFrame ? logsFrame.extraFields.filter((field) => !field?.config?.custom?.hidden) : []; + if (logsFrame?.severityField) { + otherFields.push(logsFrame?.severityField); + } + + // Use a map to dedupe labels and count their occurrences in the logs + const labelCardinality = new Map(); + + // What the label state will look like + let pendingLabelState: fieldNameMetaStore = {}; + + // If we have labels and log lines + if (labels?.length && numberOfLogLines) { + // Iterate through all of Labels + labels.forEach((labels: Labels) => { + const labelsArray = Object.keys(labels); + // Iterate through the label values + labelsArray.forEach((label) => { + // If it's already in our map, increment the count + if (labelCardinality.has(label)) { + const value = labelCardinality.get(label); + if (value) { + labelCardinality.set(label, { + percentOfLinesWithLabel: value.percentOfLinesWithLabel + 1, + active: value?.active, + }); + } + // Otherwise add it + } else { + labelCardinality.set(label, { percentOfLinesWithLabel: 1, active: undefined }); + } + }); + }); + + // Converting the map to an object + pendingLabelState = Object.fromEntries(labelCardinality); + + // Convert count to percent of log lines + Object.keys(pendingLabelState).forEach((key) => { + pendingLabelState[key].percentOfLinesWithLabel = normalize( + pendingLabelState[key].percentOfLinesWithLabel, + numberOfLogLines + ); + }); + } + + // Normalize the other fields + otherFields.forEach((field) => { + pendingLabelState[field.name] = { + percentOfLinesWithLabel: normalize( + field.values.filter((value) => value !== null && value !== undefined).length, + numberOfLogLines + ), + active: pendingLabelState[field.name]?.active, + }; + }); + + pendingLabelState = getColumnsFromProps(pendingLabelState); + + setColumnsWithMeta(pendingLabelState); + + // The panel state is updated when the user interacts with the multi-select sidebar + }, [dataFrame, getColumnsFromProps]); + + // As the number of rows change, so too must the height of the table + useEffect(() => { + setHeight(getTableHeight(dataFrame.length, false)); + }, [dataFrame.length]); + + if (!columnsWithMeta) { + return null; + } + + function columnFilterEvent(columnName: string) { + if (columnsWithMeta) { + const newState = !columnsWithMeta[columnName]?.active; + const priorActiveCount = Object.keys(columnsWithMeta).filter((column) => columnsWithMeta[column]?.active)?.length; + const event = { + columnAction: newState ? 'add' : 'remove', + columnCount: newState ? priorActiveCount + 1 : priorActiveCount - 1, + }; + + reportInteraction('grafana_explore_logs_table_column_filter_clicked', event); + } + } + + function searchFilterEvent(searchResultCount: number) { + reportInteraction('grafana_explore_logs_table_text_search_result_count', { + resultCount: searchResultCount, + }); + } + + // Toggle a column on or off when the user interacts with an element in the multi-select sidebar + const toggleColumn = (columnName: fieldName) => { + if (!columnsWithMeta || !(columnName in columnsWithMeta)) { + console.warn('failed to get column', columnsWithMeta); + return; + } + + const pendingLabelState = { + ...columnsWithMeta, + [columnName]: { ...columnsWithMeta[columnName], active: !columnsWithMeta[columnName]?.active }, + }; + + // Analytics + columnFilterEvent(columnName); + + // Set local state + setColumnsWithMeta(pendingLabelState); + + // If user is currently filtering, update filtered state + if (filteredColumnsWithMeta) { + const pendingFilteredLabelState = { + ...filteredColumnsWithMeta, + [columnName]: { ...filteredColumnsWithMeta[columnName], active: !filteredColumnsWithMeta[columnName]?.active }, + }; + setFilteredColumnsWithMeta(pendingFilteredLabelState); + } + + const newPanelState: ExploreLogsPanelState = { + ...props.panelState, + // URL format requires our array of values be an object, so we convert it using object.assign + columns: Object.assign( + {}, + // Get the keys of the object as an array + Object.keys(pendingLabelState) + // Only include active filters + .filter((key) => pendingLabelState[key]?.active) + ), + visualisationType: 'table', + }; + + // Update url state + props.updatePanelState(newPanelState); + }; + + // uFuzzy search dispatcher, adds any matches to the local state + const dispatcher = (data: string[][]) => { + const matches = data[0]; + let newColumnsWithMeta: fieldNameMetaStore = {}; + let numberOfResults = 0; + matches.forEach((match) => { + if (match in columnsWithMeta) { + newColumnsWithMeta[match] = columnsWithMeta[match]; + numberOfResults++; + } + }); + setFilteredColumnsWithMeta(newColumnsWithMeta); + searchFilterEvent(numberOfResults); + }; + + // uFuzzy search + const search = (needle: string) => { + fuzzySearch(Object.keys(columnsWithMeta), needle, dispatcher); + }; + + // Debounce fuzzy search + const debouncedSearch = debounce(search, 500); + + // onChange handler for search input + const onSearchInputChange = (e: React.FormEvent) => { + const value = e.currentTarget?.value; + if (value) { + debouncedSearch(value); + } else { + // If the search input is empty, reset the local search state. + setFilteredColumnsWithMeta(undefined); + } + }; + + const sidebarWidth = 220; + const totalWidth = props.width; + const tableWidth = totalWidth - sidebarWidth; + const styles = getStyles(props.theme, height, sidebarWidth); + + return ( +
+
+ + +
+ +
+ ); +} + +const normalize = (value: number, total: number): number => { + return Math.ceil((100 * value) / total); +}; + +function getStyles(theme: GrafanaTheme2, height: number, width: number) { + return { + wrapper: css({ + display: 'flex', + }), + sidebar: css({ + height: height, + fontSize: theme.typography.pxToRem(11), + overflowY: 'hidden', + width: width, + paddingRight: theme.spacing(1.5), + }), + + labelCount: css({}), + checkbox: css({}), + }; +} + +/** + * from public/app/features/explore/Table/TableContainer.tsx + */ +const getTableHeight = (rowCount: number, hasSubFrames: boolean) => { + if (rowCount === 0) { + return 200; + } + // 600px is pretty small for taller monitors, using the innerHeight minus an arbitrary 500px so the table can be viewed in its entirety without needing to scroll outside the panel to see the top and the bottom + const max = Math.max(window.innerHeight - 500, 600); + const min = Math.max(rowCount * 36, hasSubFrames ? 300 : 0) + 40 + 46; + // tries to estimate table height, with a min of 300 and a max of 600 + // if there are multiple tables, there is no min + return Math.min(max, min); +}; diff --git a/public/app/features/explore/Logs/utils/testMocks.test.ts b/public/app/features/explore/Logs/utils/testMocks.test.ts new file mode 100644 index 00000000000..15dea59f695 --- /dev/null +++ b/public/app/features/explore/Logs/utils/testMocks.test.ts @@ -0,0 +1,162 @@ +import { DataFrame, Field, FieldType } from '@grafana/data/src'; + +import { DataFrameType } from '../../../../../../packages/grafana-data'; + +export const getMockLokiFrame = (override?: Partial) => { + const testDataFrame: DataFrame = { + meta: { + custom: { + frameType: 'LabeledTimeValues', + }, + }, + fields: [ + { + config: {}, + name: 'labels', + type: FieldType.other, + typeInfo: { + frame: 'json.RawMessage', + }, + values: [ + { app: 'grafana', cluster: 'dev-us-central-0', container: 'hg-plugins' }, + { app: 'grafana', cluster: 'dev-us-central-1', container: 'hg-plugins' }, + { app: 'grafana', cluster: 'dev-us-central-2', container: 'hg-plugins' }, + ], + } as Field, + { + config: {}, + name: 'Time', + type: FieldType.time, + values: ['2019-01-01 10:00:00', '2019-01-01 11:00:00', '2019-01-01 12:00:00'], + }, + { + config: {}, + name: 'Line', + type: FieldType.string, + values: ['log message 1', 'log message 2', 'log message 3'], + }, + { + config: {}, + name: 'tsNs', + type: FieldType.string, + values: ['1697561006608165746', '1697560998869868000', '1697561010006578474'], + }, + { + config: {}, + name: 'id', + type: FieldType.string, + values: ['1697561006608165746_b4cc4b72', '1697560998869868000_eeb96c0f', '1697561010006578474_ad5e2e5a'], + }, + ], + length: 3, + }; + return { ...testDataFrame, ...override }; +}; +export const getMockLokiFrameDataPlane = (override?: Partial): DataFrame => { + const testDataFrame: DataFrame = { + meta: { + type: DataFrameType.LogLines, + }, + fields: [ + { + config: {}, + name: 'attributes', + type: FieldType.other, + values: [ + { app: 'grafana', cluster: 'dev-us-central-0', container: 'hg-plugins' }, + { app: 'grafana', cluster: 'dev-us-central-1', container: 'hg-plugins' }, + { app: 'grafana', cluster: 'dev-us-central-2', container: 'hg-plugins' }, + ], + }, + { + config: {}, + name: 'timestamp', + type: FieldType.time, + values: ['2019-01-01 10:00:00', '2019-01-01 11:00:00', '2019-01-01 12:00:00'], + }, + { + config: {}, + name: 'body', + type: FieldType.string, + values: ['log message 1', 'log message 2', 'log message 3'], + }, + { + config: {}, + name: 'tsNs', + type: FieldType.string, + values: ['1697561006608165746', '1697560998869868000', '1697561010006578474'], + }, + { + config: {}, + name: 'id', + type: FieldType.string, + values: ['1697561006608165746_b4cc4b72', '1697560998869868000_eeb96c0f', '1697561010006578474_ad5e2e5a'], + }, + { + config: { + links: [ + { + url: 'http://example.com', + title: 'foo', + }, + ], + }, + name: 'traceID', + type: FieldType.string, + values: ['trace1', 'trace2', 'trace3'], + }, + ], + length: 3, + }; + return { ...testDataFrame, ...override }; +}; + +export const getMockElasticFrame = (override?: Partial, timestamp = 1697732037084) => { + const testDataFrame: DataFrame = { + meta: {}, + fields: [ + { + name: '@timestamp', + type: FieldType.time, + values: [timestamp, timestamp + 1000, timestamp + 2000], + config: {}, + }, + { + name: 'line', + type: FieldType.string, + values: ['log message 1', 'log message 2', 'log message 3'], + config: {}, + }, + { + name: 'counter', + type: FieldType.string, + values: ['1', '2', '3'], + config: {}, + }, + { + name: 'level', + type: FieldType.string, + values: ['info', 'info', 'info'], + config: {}, + }, + { + name: 'id', + type: FieldType.string, + values: ['1', '2', '3'], + config: {}, + }, + ], + length: 3, + }; + return { ...testDataFrame, ...override }; +}; + +it('should return a frame', () => { + expect( + getMockLokiFrame({ + name: 'test', + }) + ).toMatchObject({ + name: 'test', + }); +}); diff --git a/public/app/features/explore/Logs/utils/uFuzzy.ts b/public/app/features/explore/Logs/utils/uFuzzy.ts new file mode 100644 index 00000000000..8804d6d52b3 --- /dev/null +++ b/public/app/features/explore/Logs/utils/uFuzzy.ts @@ -0,0 +1,45 @@ +import uFuzzy from '@leeoniya/ufuzzy'; +import { debounce as debounceLodash } from 'lodash'; + +const uf = new uFuzzy({ + intraMode: 1, + intraIns: 1, + intraSub: 1, + intraTrn: 1, + intraDel: 1, +}); + +export function fuzzySearch(haystack: string[], query: string, dispatcher: (data: string[][]) => void) { + const [idxs, info, order] = uf.search(haystack, query, false, 1e5); + + let haystackOrder: string[] = []; + let matchesSet: Set = new Set(); + if (idxs && order) { + /** + * get the fuzzy matches for hilighting + * @param part + * @param matched + */ + const mark = (part: string, matched: boolean) => { + if (matched) { + matchesSet.add(part); + } + }; + + // Iterate to create the order of needles(queries) and the matches + for (let i = 0; i < order.length; i++) { + let infoIdx = order[i]; + + /** Evaluate the match, get the matches for highlighting */ + uFuzzy.highlight(haystack[info.idx[infoIdx]], info.ranges[infoIdx], mark); + /** Get the order */ + haystackOrder.push(haystack[info.idx[infoIdx]]); + } + + dispatcher([haystackOrder, [...matchesSet]]); + } else if (!query) { + dispatcher([[], []]); + } +} + +export const debouncedFuzzySearch = debounceLodash(fuzzySearch, 300); diff --git a/public/app/features/explore/hooks/useStateSync/index.ts b/public/app/features/explore/hooks/useStateSync/index.ts index 78575430e39..22caa8be3ce 100644 --- a/public/app/features/explore/hooks/useStateSync/index.ts +++ b/public/app/features/explore/hooks/useStateSync/index.ts @@ -10,7 +10,7 @@ import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSou import { addListener, ExploreItemState, ExploreQueryParams, useDispatch, useSelector } from 'app/types'; import { changeDatasource } from '../../state/datasource'; -import { initializeExplore } from '../../state/explorePane'; +import { changePanelsStateAction, initializeExplore } from '../../state/explorePane'; import { clearPanes, splitClose, splitOpen, syncTimesAction } from '../../state/main'; import { runQueries, setQueriesAction } from '../../state/query'; import { selectPanes } from '../../state/selectors'; @@ -49,9 +49,14 @@ export function useStateSync(params: ExploreQueryParams) { // - a pane is opened or closed // - a query is run // - range is changed - [splitClose.type, splitOpen.fulfilled.type, runQueries.pending.type, changeRangeAction.type].includes( - action.type - ), + // - panel state is updated + [ + splitClose.type, + splitOpen.fulfilled.type, + runQueries.pending.type, + changeRangeAction.type, + changePanelsStateAction.type, + ].includes(action.type), effect: async (_, { cancelActiveListeners, delay, getState }) => { // The following 2 lines will throttle updates to avoid creating history entries when rapid changes // are committed to the store. @@ -128,6 +133,10 @@ export function useStateSync(params: ExploreQueryParams) { if (update.queries || update.range) { dispatch(runQueries({ exploreId })); } + + if (update.panelsState && panelsState) { + dispatch(changePanelsStateAction({ exploreId, panelsState })); + } }); } else { // This happens when browser history is used to navigate. diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index 1ce86f1e245..d84551add59 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -53,7 +53,7 @@ interface ChangePanelsState { exploreId: string; panelsState: ExplorePanelsState; } -const changePanelsStateAction = createAction('explore/changePanels'); +export const changePanelsStateAction = createAction('explore/changePanels'); export function changePanelState( exploreId: string, panel: PreferredVisualisationType, From 7d619199d0c04c08717279be6ac19d7857bfe65b Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 26 Oct 2023 10:46:45 -0700 Subject: [PATCH 108/180] K8s/Authorizer: Move allow from fallback to org_role (#77235) --- .../grafana-apiserver/auth/authorizer/org/org_role.go | 9 +++++---- .../grafana-apiserver/auth/authorizer/provider.go | 9 +++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/pkg/services/grafana-apiserver/auth/authorizer/org/org_role.go b/pkg/services/grafana-apiserver/auth/authorizer/org/org_role.go index 93268f976cc..3776da4377d 100644 --- a/pkg/services/grafana-apiserver/auth/authorizer/org/org_role.go +++ b/pkg/services/grafana-apiserver/auth/authorizer/org/org_role.go @@ -4,10 +4,11 @@ import ( "context" "fmt" + "k8s.io/apiserver/pkg/authorization/authorizer" + "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/org" - "k8s.io/apiserver/pkg/authorization/authorizer" ) var _ authorizer.Authorizer = &OrgIDAuthorizer{} @@ -28,18 +29,18 @@ func (auth OrgRoleAuthorizer) Authorize(ctx context.Context, a authorizer.Attrib switch signedInUser.OrgRole { case org.RoleAdmin: - return authorizer.DecisionNoOpinion, "", nil + return authorizer.DecisionAllow, "", nil case org.RoleEditor: switch a.GetVerb() { case "get", "list", "watch", "create", "update", "patch", "delete", "put", "post": - return authorizer.DecisionNoOpinion, "", nil + return authorizer.DecisionAllow, "", nil default: return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(string(signedInUser.OrgRole), a), nil } case org.RoleViewer: switch a.GetVerb() { case "get", "list", "watch": - return authorizer.DecisionNoOpinion, "", nil + return authorizer.DecisionAllow, "", nil default: return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(string(signedInUser.OrgRole), a), nil } diff --git a/pkg/services/grafana-apiserver/auth/authorizer/provider.go b/pkg/services/grafana-apiserver/auth/authorizer/provider.go index 4441da7dbe0..c658252bb41 100644 --- a/pkg/services/grafana-apiserver/auth/authorizer/provider.go +++ b/pkg/services/grafana-apiserver/auth/authorizer/provider.go @@ -28,11 +28,8 @@ func ProvideAuthorizer( authorizers = append(authorizers, orgIDAuthorizer) } - authorizers = append(authorizers, - orgRoleAuthorizer, - - // Add this last so that if nothing says authorizer.DecisionDeny, it will pass - authorizerfactory.NewAlwaysAllowAuthorizer(), - ) + // org role is last -- and will return allow for verbs that match expectations + // Ideally FGAC happens earlier and returns an explicit answer + authorizers = append(authorizers, orgRoleAuthorizer) return union.New(authorizers...) } From b2ef9a81f2591e78410e658eb4f8c3fa59763c15 Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Thu, 26 Oct 2023 14:52:27 -0300 Subject: [PATCH 109/180] PublicDashboards: Chore refactor api test (#77091) --- pkg/services/publicdashboards/api/api_test.go | 123 +++++++++--------- 1 file changed, 64 insertions(+), 59 deletions(-) diff --git a/pkg/services/publicdashboards/api/api_test.go b/pkg/services/publicdashboards/api/api_test.go index 098b186120e..176bacd7aed 100644 --- a/pkg/services/publicdashboards/api/api_test.go +++ b/pkg/services/publicdashboards/api/api_test.go @@ -27,10 +27,6 @@ var userAdmin = &user.SignedInUser{UserID: 2, OrgID: 1, OrgRole: org.RoleAdmin, var userViewer = &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleViewer, Login: "testViewerUserRBAC", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll}}}} var anonymousUser = &user.SignedInUser{IsAnonymous: true} -type JsonErrResponse struct { - Error string `json:"error"` -} - func TestAPIFeatureFlag(t *testing.T) { testCases := []struct { Name string @@ -512,130 +508,139 @@ func TestApiCreatePublicDashboard(t *testing.T) { func TestAPIUpdatePublicDashboard(t *testing.T) { dashboardUid := "abc1234" publicDashboardUid := "1234asdfasdf" - - adminUser := &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleEditor, Login: "testEditorUser", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {dashboards.ScopeDashboardsAll}}}} - - userEditorPublicDashboard := &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleEditor, Login: "testEditorUser", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {fmt.Sprintf("dashboards:uid:%s", dashboardUid)}}}} - - userEditorAnotherPublicDashboard := &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleEditor, Login: "testEditorUser", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {"another-uid"}}}} + userEditorPublicDashboard := &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleEditor, Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {fmt.Sprintf("dashboards:uid:%s", dashboardUid)}}}} + userEditorAnotherPublicDashboard := &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleEditor, Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {"another-uid"}}}} testCases := []struct { Name string User *user.SignedInUser DashboardUid string PublicDashboardUid string - PublicDashboardRes *PublicDashboard - PublicDashboardErr error + Body string + ExpectedResponse *PublicDashboard + ExpectedError interface{} ExpectedHttpResponse int ShouldCallService bool }{ { - Name: "Invalid dashboardUid", - User: adminUser, - DashboardUid: "", - PublicDashboardUid: "", - PublicDashboardRes: nil, - PublicDashboardErr: ErrPublicDashboardIdentifierNotSet.Errorf(""), - ExpectedHttpResponse: http.StatusNotFound, + Name: "Invalid dashboard uid bad request error", + User: userAdmin, + DashboardUid: ".", + PublicDashboardUid: publicDashboardUid, + Body: fmt.Sprintf(`{ "uid": "%s"}`, publicDashboardUid), + ExpectedResponse: nil, + ExpectedError: ErrInvalidUid.Errorf(""), + ExpectedHttpResponse: http.StatusBadRequest, ShouldCallService: false, }, { - Name: "Invalid public dashboard uid", - User: adminUser, + Name: "Invalid public dashboard uid bad request error", + User: userAdmin, DashboardUid: dashboardUid, - PublicDashboardUid: "", - PublicDashboardRes: nil, - PublicDashboardErr: ErrPublicDashboardNotFound.Errorf(""), - ExpectedHttpResponse: http.StatusNotFound, + PublicDashboardUid: ".", + Body: fmt.Sprintf(`{ "uid": "%s"}`, publicDashboardUid), + ExpectedResponse: nil, + ExpectedError: ErrInvalidUid.Errorf(""), + ExpectedHttpResponse: http.StatusBadRequest, ShouldCallService: false, }, { - Name: "Service Error", - User: adminUser, + Name: "Dashboard not found error", + User: userAdmin, DashboardUid: dashboardUid, PublicDashboardUid: publicDashboardUid, - PublicDashboardRes: nil, - PublicDashboardErr: ErrDashboardNotFound.Errorf(""), + Body: fmt.Sprintf(`{ "uid": "%s"}`, publicDashboardUid), + ExpectedResponse: nil, + ExpectedError: ErrDashboardNotFound.Errorf(""), ExpectedHttpResponse: http.StatusNotFound, ShouldCallService: true, }, { Name: "Success", - User: adminUser, + User: userAdmin, DashboardUid: dashboardUid, PublicDashboardUid: publicDashboardUid, - PublicDashboardRes: &PublicDashboard{Uid: "success"}, - PublicDashboardErr: nil, + Body: fmt.Sprintf(`{ "uid": "%s"}`, publicDashboardUid), + ExpectedResponse: &PublicDashboard{Uid: "success"}, + ExpectedError: nil, ExpectedHttpResponse: http.StatusOK, ShouldCallService: true, }, - - // permissions { - Name: "User can update this public dashboard", + Name: "Invalid payload bad request error", + User: userAdmin, + DashboardUid: dashboardUid, + PublicDashboardUid: publicDashboardUid, + Body: `{nonvalidjson,`, + ExpectedResponse: nil, + ExpectedError: ErrBadRequest.Errorf(""), + ExpectedHttpResponse: http.StatusBadRequest, + ShouldCallService: false, + }, + { + Name: "User has permissions to update this public dashboard", User: userEditorPublicDashboard, DashboardUid: dashboardUid, PublicDashboardUid: publicDashboardUid, - PublicDashboardRes: &PublicDashboard{Uid: "success"}, - PublicDashboardErr: nil, + Body: fmt.Sprintf(`{ "uid": "%s"}`, publicDashboardUid), + ExpectedResponse: &PublicDashboard{Uid: "success"}, + ExpectedError: nil, ExpectedHttpResponse: http.StatusOK, ShouldCallService: true, }, { - Name: "User has permissions on another dashboard", + Name: "User has permissions to update another dashboard but not the requested one", User: userEditorAnotherPublicDashboard, + DashboardUid: dashboardUid, PublicDashboardUid: publicDashboardUid, + Body: fmt.Sprintf(`{ "uid": "%s"}`, publicDashboardUid), ExpectedHttpResponse: http.StatusForbidden, ShouldCallService: false, }, { - Name: "Viewer cannot update any dashboard", + Name: "User Viewer cannot update any dashboard", User: userViewer, + DashboardUid: dashboardUid, PublicDashboardUid: publicDashboardUid, + Body: fmt.Sprintf(`{ "uid": "%s"}`, publicDashboardUid), ExpectedHttpResponse: http.StatusForbidden, ShouldCallService: false, }, } for _, test := range testCases { + cfg := setting.NewCfg() + features := featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards) + t.Run(test.Name, func(t *testing.T) { service := publicdashboards.NewFakePublicDashboardService(t) if test.ShouldCallService { service.On("Update", mock.Anything, mock.Anything, mock.Anything). - Return(test.PublicDashboardRes, test.PublicDashboardErr) + Return(test.ExpectedResponse, test.ExpectedError) } - cfg := setting.NewCfg() - features := featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards) testServer := setupTestServer(t, cfg, features, service, nil, test.User) url := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards/%s", test.DashboardUid, test.PublicDashboardUid) - body := strings.NewReader(fmt.Sprintf(`{ "uid": "%s"}`, test.PublicDashboardUid)) + body := strings.NewReader(test.Body) response := callAPI(testServer, http.MethodPatch, url, body, t) assert.Equal(t, test.ExpectedHttpResponse, response.Code) - // check whether service called - if !test.ShouldCallService { - service.AssertNotCalled(t, "Update") - } - - fmt.Println(response.Body.String()) - - // check response - if response.Code == http.StatusOK { - val, err := json.Marshal(test.PublicDashboardRes) + // check response when expected response is 200 + if test.ExpectedHttpResponse == http.StatusOK { + val, err := json.Marshal(test.ExpectedResponse) require.NoError(t, err) assert.Equal(t, string(val), response.Body.String()) + } - // verify 4XXs except 403 && 404 - } else if test.ExpectedHttpResponse > 200 && - test.ExpectedHttpResponse != 403 && - test.ExpectedHttpResponse != 404 { - var errResp JsonErrResponse + // forbidden status is returned by middleware and does not have the format of the errutil.PublicError + if test.ExpectedHttpResponse != http.StatusOK && test.ExpectedHttpResponse != http.StatusForbidden { + var errResp errutil.PublicError err := json.Unmarshal(response.Body.Bytes(), &errResp) require.NoError(t, err) - assert.Equal(t, test.PublicDashboardErr.Error(), errResp.Error) + assert.Equal(t, test.ExpectedHttpResponse, errResp.StatusCode) + assert.Equal(t, test.ExpectedError.(errutil.Error).MessageID, errResp.MessageID) } }) } From d3fce96571f6de6c426a96a2cfb223ef4fa33b7a Mon Sep 17 00:00:00 2001 From: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> Date: Thu, 26 Oct 2023 13:31:09 -0600 Subject: [PATCH 110/180] =?UTF-8?q?docs:=20What=E2=80=99s=20new=20addition?= =?UTF-8?q?=20(#77233)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * baldm0mma/whats_new_msi/ add content and screenshot * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * baldm0mma/whats_new_msi/ update directions * baldm0mma/whats_new_msi * baldm0mma/whats_new_msi/ update image and max-width * Small wording fix to put things in present tense * baldm0mma/whats_new_msi/ update with suggestions * baldm0mma/whats_new_msi/ update figure * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * baldm0mma/whats_new_msi/ update image * Update docs/sources/whatsnew/whats-new-in-v10-2.md * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v10-2.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> --------- Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> --- docs/sources/whatsnew/whats-new-in-v10-2.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/sources/whatsnew/whats-new-in-v10-2.md b/docs/sources/whatsnew/whats-new-in-v10-2.md index 1da21a0fa2e..08ea157f92f 100644 --- a/docs/sources/whatsnew/whats-new-in-v10-2.md +++ b/docs/sources/whatsnew/whats-new-in-v10-2.md @@ -277,6 +277,24 @@ To learn more, refer to [Datadog data source settings](/docs/plugins/grafana-dat {{< video-embed src="/media/docs/datadog/datadog-rate-limit.mp4" >}} +### Microsoft SQL Server: Support for Azure Authentication (Service principal/MSI) + + + +_Generally available in all editions of Grafana_ + +We've added support for Azure Authentication (Service principal/MSI) on our MS SQL plugin to authenticate and allow querying of content stored in SQL Managed Instance databases. + +Enable this feature by setting the `managed_identity_enabled` property to `true` under the `Azure` heading in your configuration file (/conf/.ini). Then take the following steps in your Microsoft SQL Server data source configuration UI: + +1. Under **Authentication**, select **Azure AD Authentication** in the drop-down to reveal the **Azure Authentication Settings** section. +2. In this section, select either **Managed Identity** or **App Registration**. +3. Enter the credentials accordingly. + +{{< figure src="/media/docs/grafana/data-sources/screenshot-managed-identity-mssql-ui-cropped.png" caption="Azure MSI Authentication" max-width="550px" >}} + +Learn more in the [Microsoft SQL Server documentation](https://grafana.com/docs/grafana//datasources/mssql/). + ## Transformations As our work on improving the user experience of transforming data continues, we've also been adding new capabilities to transformations. From 72a085ea208642e5aaf5aae730bd44eb792cc9ba Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Thu, 26 Oct 2023 16:08:52 -0400 Subject: [PATCH 111/180] Cloudwatch: Add DB_PERF_INSIGHTS to Metric Math (#77241) --- .../datasource/cloudwatch/language/metric-math/language.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/plugins/datasource/cloudwatch/language/metric-math/language.ts b/public/app/plugins/datasource/cloudwatch/language/metric-math/language.ts index 0c2de35a879..a26d879a9e4 100644 --- a/public/app/plugins/datasource/cloudwatch/language/metric-math/language.ts +++ b/public/app/plugins/datasource/cloudwatch/language/metric-math/language.ts @@ -7,6 +7,7 @@ export const METRIC_MATH_FNS = [ 'AVG', 'CEIL', 'DATAPOINT_COUNT', + 'DB_PERF_INSIGHTS', 'DIFF', 'DIFF_TIME', 'FILL', From 45aa58c4a8768a418b15048c4a8bfc620e02f664 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Thu, 26 Oct 2023 23:23:01 -0500 Subject: [PATCH 112/180] Histogram: Render heatmap-cells and heatmap-rows frames (#77111) --- .betterer.results | 5 +- .../transformations/transformers/histogram.ts | 89 +++++++++++++++++ public/app/plugins/panel/heatmap/utils.ts | 10 +- .../app/plugins/panel/histogram/Histogram.tsx | 96 ++++++++++++------- 4 files changed, 160 insertions(+), 40 deletions(-) diff --git a/.betterer.results b/.betterer.results index 00598e6a077..196906a6074 100644 --- a/.betterer.results +++ b/.betterer.results @@ -7263,9 +7263,10 @@ exports[`better eslint`] = { ], "public/app/plugins/panel/histogram/Histogram.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] + [0, 0, 0, "Do not use any type assertions.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], "public/app/plugins/panel/live/LiveChannelEditor.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/packages/grafana-data/src/transformations/transformers/histogram.ts b/packages/grafana-data/src/transformations/transformers/histogram.ts index a82217c2467..1a8f8d858e7 100644 --- a/packages/grafana-data/src/transformations/transformers/histogram.ts +++ b/packages/grafana-data/src/transformations/transformers/histogram.ts @@ -183,6 +183,95 @@ export interface HistogramFields { * @alpha */ export function getHistogramFields(frame: DataFrame): HistogramFields | undefined { + // we ignore xMax (time field) and sum all counts together for each found bucket + if (frame.meta?.type === DataFrameType.HeatmapCells) { + // we assume uniform bucket size for now + // we assume xMax, yMin, yMax fields + let yMinField = frame.fields.find((f) => f.name === 'yMin')!; + let yMaxField = frame.fields.find((f) => f.name === 'yMax')!; + let countField = frame.fields.find((f) => f.name === 'count')!; + + let uniqueMaxs = [...new Set(yMaxField.values)].sort((a, b) => a - b); + let uniqueMins = [...new Set(yMinField.values)].sort((a, b) => a - b); + let countsByMax = new Map(); + uniqueMaxs.forEach((max) => countsByMax.set(max, 0)); + + for (let i = 0; i < yMaxField.values.length; i++) { + let max = yMaxField.values[i]; + countsByMax.set(max, countsByMax.get(max) + countField.values[i]); + } + + let fields = { + xMin: { + ...yMinField, + name: 'xMin', + values: uniqueMins, + }, + xMax: { + ...yMaxField, + name: 'xMax', + values: uniqueMaxs, + }, + counts: [ + { + ...countField, + values: [...countsByMax.values()], + }, + ], + }; + + return fields; + } else if (frame.meta?.type === DataFrameType.HeatmapRows) { + // assumes le + + // tick label strings (will be ordinal-ized) + let minVals: string[] = []; + let maxVals: string[] = []; + + // sums of all timstamps per bucket + let countVals: number[] = []; + + let minVal = '0'; + frame.fields.forEach((f) => { + if (f.type === FieldType.number) { + let countsSum = f.values.reduce((acc, v) => acc + v, 0); + countVals.push(countsSum); + minVals.push(minVal); + maxVals.push((minVal = f.name)); + } + }); + + // fake extra value for +Inf (for x scale ranging since bars are right-aligned) + countVals.push(0); + minVals.push(minVal); + maxVals.push(minVal); + + let fields = { + xMin: { + ...frame.fields[1], + name: 'xMin', + type: FieldType.string, + values: minVals, + }, + xMax: { + ...frame.fields[1], + name: 'xMax', + type: FieldType.string, + values: maxVals, + }, + counts: [ + { + ...frame.fields[1], + name: 'count', + type: FieldType.number, + values: countVals, + }, + ], + }; + + return fields; + } + let xMin: Field | undefined = undefined; let xMax: Field | undefined = undefined; const counts: Field[] = []; diff --git a/public/app/plugins/panel/heatmap/utils.ts b/public/app/plugins/panel/heatmap/utils.ts index d5d09e238c1..7dcbf6643a6 100644 --- a/public/app/plugins/panel/heatmap/utils.ts +++ b/public/app/plugins/panel/heatmap/utils.ts @@ -314,6 +314,12 @@ export function prepConfig(opts: PrepConfigOpts) { // sparse already accounts for le/ge by explicit yMin & yMax cell bounds, so no need to expand y range isSparseHeatmap ? (u, dataMin, dataMax) => { + // ...but uPlot currently only auto-ranges from the yMin facet data, so we have to grow by 1 extra factor + // @ts-ignore + let bucketFactor = u.data[1][2][0] / u.data[1][1][0]; + + dataMax *= bucketFactor; + let scaleMin: number | null, scaleMax: number | null; [scaleMin, scaleMax] = shouldUseLogScale @@ -900,8 +906,8 @@ export function heatmapPathsSparse(opts: PathbuilderOpts) { xSize = Math.max(1, xSize - cellGap); ySize = Math.max(1, ySize - cellGap); - let x = xMaxPx; - let y = yMinPx; + let x = xMaxPx - cellGap / 2 - xSize; + let y = yMaxPx + cellGap / 2; let fillPath = fillPaths[fills[i]]; diff --git a/public/app/plugins/panel/histogram/Histogram.tsx b/public/app/plugins/panel/histogram/Histogram.tsx index 58ec3eab141..77d3c3127bd 100644 --- a/public/app/plugins/panel/histogram/Histogram.tsx +++ b/public/app/plugins/panel/histogram/Histogram.tsx @@ -3,6 +3,7 @@ import uPlot, { AlignedData } from 'uplot'; import { DataFrame, + FieldType, formattedValueToString, getFieldColorModeForField, getFieldSeriesColor, @@ -47,7 +48,12 @@ export interface HistogramProps extends Themeable2 { export function getBucketSize(frame: DataFrame) { // assumes BucketMin is fields[0] and BucktMax is fields[1] - return frame.fields[1].values[0] - frame.fields[0].values[0]; + return frame.fields[0].type === FieldType.string ? 1 : frame.fields[1].values[0] - frame.fields[0].values[0]; +} + +export function getBucketSize1(frame: DataFrame) { + // assumes BucketMin is fields[0] and BucktMax is fields[1] + return frame.fields[0].type === FieldType.string ? 1 : frame.fields[1].values[1] - frame.fields[0].values[1]; } const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { @@ -59,8 +65,15 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { let builder = new UPlotConfigBuilder(); + let isOrdinalX = frame.fields[0].type === FieldType.string; + // assumes BucketMin is fields[0] and BucktMax is fields[1] let bucketSize = getBucketSize(frame); + let bucketSize1 = getBucketSize1(frame); + + let bucketFactor = bucketSize1 / bucketSize; + + let useLogScale = bucketSize1 !== bucketSize; // (imperfect floats) // splits shifter, to ensure splits always start at first bucket let xSplits: uPlot.Axis.Splits = (u, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace) => { @@ -84,35 +97,44 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { builder.addScale({ scaleKey: 'x', // bukkits isTime: false, - distribution: ScaleDistribution.Linear, + distribution: isOrdinalX + ? ScaleDistribution.Ordinal + : useLogScale + ? ScaleDistribution.Log + : ScaleDistribution.Linear, + log: 2, orientation: ScaleOrientation.Horizontal, direction: ScaleDirection.Right, - range: (u, wantedMin, wantedMax) => { - // these settings will prevent zooming, probably okay? - if (xScaleMin != null) { - wantedMin = xScaleMin; - } - if (xScaleMax != null) { - wantedMax = xScaleMax; - } + range: useLogScale + ? (u, wantedMin, wantedMax) => { + return uPlot.rangeLog(wantedMin, wantedMax * bucketFactor, 2, true); + } + : (u, wantedMin, wantedMax) => { + // these settings will prevent zooming, probably okay? + if (xScaleMin != null) { + wantedMin = xScaleMin; + } + if (xScaleMax != null) { + wantedMax = xScaleMax; + } - let fullRangeMin = u.data[0][0]; - let fullRangeMax = u.data[0][u.data[0].length - 1]; + let fullRangeMin = u.data[0][0]; + let fullRangeMax = u.data[0][u.data[0].length - 1]; - // snap to bucket divisors... + // snap to bucket divisors... - if (wantedMax === fullRangeMax) { - wantedMax += bucketSize; - } else { - wantedMax = incrRoundUp(wantedMax, bucketSize); - } + if (wantedMax === fullRangeMax) { + wantedMax += bucketSize; + } else { + wantedMax = incrRoundUp(wantedMax, bucketSize); + } - if (wantedMin > fullRangeMin) { - wantedMin = incrRoundDn(wantedMin, bucketSize); - } + if (wantedMin > fullRangeMin) { + wantedMin = incrRoundDn(wantedMin, bucketSize); + } - return [wantedMin, wantedMax]; - }, + return [wantedMin, wantedMax]; + }, }); builder.addScale({ @@ -132,22 +154,24 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { scaleKey: 'x', isTime: false, placement: AxisPlacement.Bottom, - incrs: histogramBucketSizes, - splits: xSplits, - values: (u: uPlot, splits: any[]) => { - const tickLabels = splits.map(xAxisFormatter); + incrs: isOrdinalX ? [1] : useLogScale ? undefined : histogramBucketSizes, + splits: useLogScale || isOrdinalX ? undefined : xSplits, + values: isOrdinalX + ? (u: uPlot, splits: any[]) => splits + : (u: uPlot, splits: any[]) => { + const tickLabels = splits.map(xAxisFormatter); - const maxWidth = tickLabels.reduce( - (curMax, label) => Math.max(measureText(label, UPLOT_AXIS_FONT_SIZE).width, curMax), - 0 - ); + const maxWidth = tickLabels.reduce( + (curMax, label) => Math.max(measureText(label, UPLOT_AXIS_FONT_SIZE).width, curMax), + 0 + ); - const labelSpacing = 10; - const maxCount = u.bbox.width / ((maxWidth + labelSpacing) * devicePixelRatio); - const keepMod = Math.ceil(tickLabels.length / maxCount); + const labelSpacing = 10; + const maxCount = u.bbox.width / ((maxWidth + labelSpacing) * devicePixelRatio); + const keepMod = Math.ceil(tickLabels.length / maxCount); - return tickLabels.map((label, i) => (i % keepMod === 0 ? label : null)); - }, + return tickLabels.map((label, i) => (i % keepMod === 0 ? label : null)); + }, //incrs: () => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((mult) => mult * bucketSize), //splits: config.xSplits, //values: config.xValues, From 7d2bfea7771c0716c46455d2d629430f4fa78383 Mon Sep 17 00:00:00 2001 From: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> Date: Fri, 27 Oct 2023 02:09:05 -0400 Subject: [PATCH 113/180] Pyroscope: Added app integration for datasource (#75789) * feat: integrate pyroscope query editor with link extensions - allows plugin app extensions to register links based on the query and selected datasource * fix: remove not-as-designed busy wait mechanism * Apply suggestions from code review Co-authored-by: Marcus Andersson * fix: implement feedback * fix: lint --------- Co-authored-by: Marcus Andersson --- .../QueryEditor/QueryEditor.test.tsx | 7 + .../QueryEditor/QueryEditor.tsx | 2 + .../QueryEditor/QueryLinkExtension.test.tsx | 125 ++++++++++++++++++ .../QueryEditor/QueryLinkExtension.tsx | 104 +++++++++++++++ .../datasource.test.ts | 32 ++++- 5 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryLinkExtension.test.tsx create mode 100644 public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryLinkExtension.tsx diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.test.tsx b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.test.tsx index 07e4c3de2b4..75c0f98e564 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.test.tsx +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.test.tsx @@ -3,13 +3,20 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { CoreApp, PluginType } from '@grafana/data'; +import { setPluginExtensionGetter } from '@grafana/runtime'; import { PyroscopeDataSource } from '../datasource'; +import { mockFetchPyroscopeDatasourceSettings } from '../datasource.test'; import { ProfileTypeMessage } from '../types'; import { Props, QueryEditor } from './QueryEditor'; describe('QueryEditor', () => { + beforeEach(() => { + setPluginExtensionGetter(() => ({ extensions: [] })); // No extensions + mockFetchPyroscopeDatasourceSettings(); + }); + it('should render without error', async () => { setup(); diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.tsx b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.tsx index be3ca6eef6e..344d12820b1 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.tsx @@ -12,6 +12,7 @@ import { EditorRow } from './EditorRow'; import { EditorRows } from './EditorRows'; import { LabelsEditor } from './LabelsEditor'; import { ProfileTypesCascader, useProfileTypes } from './ProfileTypesCascader'; +import { PyroscopeQueryLinkExtensions } from './QueryLinkExtension'; import { QueryOptions } from './QueryOptions'; export type Props = QueryEditorProps; @@ -57,6 +58,7 @@ export function QueryEditor(props: Props) { labels={labels} getLabelValues={getLabelValues} /> + diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryLinkExtension.test.tsx b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryLinkExtension.test.tsx new file mode 100644 index 00000000000..28bfef9ecf6 --- /dev/null +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryLinkExtension.test.tsx @@ -0,0 +1,125 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { act } from 'react-dom/test-utils'; + +import { PluginType, rangeUtil, PluginExtensionLink, PluginExtensionTypes } from '@grafana/data'; +import { getPluginLinkExtensions } from '@grafana/runtime'; + +import { PyroscopeDataSource } from '../datasource'; +import { mockFetchPyroscopeDatasourceSettings } from '../datasource.test'; + +import { Props, PyroscopeQueryLinkExtensions, resetPyroscopeQueryLinkExtensionsFetches } from './QueryLinkExtension'; + +// Constants copied from `QueryLinkExtension.tsx` +const EXTENSION_POINT_ID = 'plugins/grafana-pyroscope-datasource/query-links'; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + setPluginExtensionGetter: jest.fn(), + getPluginLinkExtensions: jest.fn(), +})); + +const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions); + +const defaultPyroscopeDataSourceSettings = { + uid: 'default-pyroscope', + url: 'http://pyroscope', + basicAuthUser: 'pyroscope_user', +}; + +describe('PyroscopeQueryLinkExtensions', () => { + const EXPECTED_BUTTON_LABEL = 'Profiles App'; + const DEFAULT_EXTENSION_PATH = 'a/mock-path-app/fake-path'; + + function createExtension(overrides?: Partial) { + return { + ...{ + description: 'unremarkable-description', + extensionPointId: EXTENSION_POINT_ID, + title: EXPECTED_BUTTON_LABEL, + path: DEFAULT_EXTENSION_PATH, + type: PluginExtensionTypes.link, + category: 'unremarkable-category', + icon: 'heart', + onClick() {}, + pluginId: 'mock-path-app', + id: `${Date.now()}}`, + }, + ...overrides, + } as PluginExtensionLink; + } + + beforeEach(() => { + resetPyroscopeQueryLinkExtensionsFetches(); + mockFetchPyroscopeDatasourceSettings(defaultPyroscopeDataSourceSettings); + + getPluginLinkExtensionsMock.mockRestore(); + getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] }); // Unless stated otherwise, no extensions + }); + + it('should render if extension present', async () => { + getPluginLinkExtensionsMock.mockReturnValue({ extensions: [createExtension()] }); // Default extension + + await act(setup); + expect(await screen.findAllByText(EXPECTED_BUTTON_LABEL)).toBeDefined(); + }); + + it('Should not render if no extension present', async () => { + await act(setup); + expect(screen.queryByText(EXPECTED_BUTTON_LABEL)).toBeNull(); + }); +}); + +function setupDs() { + const ds = new PyroscopeDataSource({ + ...defaultPyroscopeDataSourceSettings, + name: 'test', + type: PluginType.datasource, + access: 'proxy', + id: 1, + jsonData: {}, + meta: { + name: '', + id: '', + type: PluginType.datasource, + baseUrl: '', + info: { + author: { + name: '', + }, + description: '', + links: [], + logos: { + large: '', + small: '', + }, + screenshots: [], + updated: '', + version: '', + }, + module: '', + }, + readOnly: false, + }); + + return ds; +} + +async function setup(options: { props: Partial } = { props: {} }) { + const utils = render( + + ); + return { ...utils }; +} diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryLinkExtension.tsx b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryLinkExtension.tsx new file mode 100644 index 00000000000..21a8a727ad5 --- /dev/null +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryLinkExtension.tsx @@ -0,0 +1,104 @@ +import { css } from '@emotion/css'; +import React from 'react'; +import { useAsync } from 'react-use'; + +import { GrafanaTheme2, QueryEditorProps, TimeRange } from '@grafana/data'; +import { getBackendSrv, getPluginLinkExtensions } from '@grafana/runtime'; +import { LinkButton, useStyles2 } from '@grafana/ui'; + +import { PyroscopeDataSource } from '../datasource'; +import { PyroscopeDataSourceOptions, Query } from '../types'; + +const EXTENSION_POINT_ID = 'plugins/grafana-pyroscope-datasource/query-links'; + +/** A subset of the datasource settings that are relevant for this integration */ +type PyroscopeDatasourceSettings = { + uid: string; + url: string; + type: string; + basicAuthUser: string; +}; + +/** The context object that will be shared with the link extension's configure function */ +type ExtensionQueryLinksContext = { + datasourceUid: string; + query: Query; + range?: TimeRange | undefined; + datasourceSettings?: PyroscopeDatasourceSettings; +}; + +/* Global promises to fetch pyroscope datasource settings by uid as encountered */ +const pyroscopeDatasourceSettingsByUid: Record = {}; + +/* Reset promises for testing purposes */ +export function resetPyroscopeQueryLinkExtensionsFetches() { + Object.keys(pyroscopeDatasourceSettingsByUid).forEach((key) => delete pyroscopeDatasourceSettingsByUid[key]); +} + +/** A subset of the `PyroscopeDataSource` `QueryEditorProps` */ +export type Props = Pick< + QueryEditorProps, + 'datasource' | 'query' | 'range' +>; + +export function PyroscopeQueryLinkExtensions(props: Props) { + const { + datasource: { uid: datasourceUid }, + query, + range, + } = props; + + const { value: datasourceSettings } = useAsync(async () => { + if (pyroscopeDatasourceSettingsByUid[datasourceUid]) { + return pyroscopeDatasourceSettingsByUid[datasourceUid]; + } + const settings = await getBackendSrv().get(`/api/datasources/uid/${datasourceUid}`); + pyroscopeDatasourceSettingsByUid[datasourceUid] = settings; + return settings; + }, [datasourceUid]); + + const context: ExtensionQueryLinksContext = { + datasourceUid, + query, + range, + datasourceSettings, + }; + + const { extensions } = getPluginLinkExtensions({ + extensionPointId: EXTENSION_POINT_ID, + context, + }); + + const styles = useStyles2(getStyles); + + if (extensions.length === 0) { + return null; + } + + return ( + <> + {extensions.map((extension) => ( + + {extension.title} + + ))} + + ); +} + +function getStyles(theme: GrafanaTheme2) { + return { + linkButton: css({ + marginLeft: theme.spacing(1), + }), + }; +} diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.test.ts b/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.test.ts index c9f09aaa38c..f2c296e31eb 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.test.ts +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.test.ts @@ -1,13 +1,43 @@ -import { AbstractLabelOperator, CoreApp, DataSourceInstanceSettings, PluginMetaInfo, PluginType } from '@grafana/data'; +import { + AbstractLabelOperator, + CoreApp, + DataSourceInstanceSettings, + PluginMetaInfo, + PluginType, + DataSourceJsonData, +} from '@grafana/data'; +import { setPluginExtensionGetter, getBackendSrv, setBackendSrv } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { defaultPyroscopeQueryType } from './dataquery.gen'; import { normalizeQuery, PyroscopeDataSource } from './datasource'; import { Query } from './types'; +/** The datasource QueryEditor fetches datasource settings to send to the extension's `configure` method */ +export function mockFetchPyroscopeDatasourceSettings( + datasourceSettings?: Partial> +) { + const settings = { ...defaultSettings, ...datasourceSettings }; + const returnValues: Record = { + [`/api/datasources/uid/${settings.uid}`]: settings, + }; + setBackendSrv({ + ...getBackendSrv(), + get: function (path: string) { + const value = returnValues[path]; + if (value) { + return Promise.resolve(value as T); + } + return Promise.reject({ message: 'reject' }); + }, + }); +} + describe('Pyroscope data source', () => { let ds: PyroscopeDataSource; beforeEach(() => { + mockFetchPyroscopeDatasourceSettings(); + setPluginExtensionGetter(() => ({ extensions: [] })); // No extensions ds = new PyroscopeDataSource(defaultSettings); }); From 1b6d39f8239551afef24d3220792299061e334e2 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Fri, 27 Oct 2023 08:30:33 +0200 Subject: [PATCH 114/180] IDForwarding: Require that id forwarding is enabled for data source (#77131) * Require that id forwarding is enabled for data source * Address feedback --- pkg/api/pluginproxy/ds_proxy.go | 3 +- pkg/services/auth/id.go | 8 ++++ .../clientmiddleware/forward_id_middleware.go | 15 ++++++- .../forward_id_middleware_test.go | 44 +++++++++++++++++-- 4 files changed, 65 insertions(+), 5 deletions(-) diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index 35c1ca2efc5..87c449d13c1 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -19,6 +19,7 @@ import ( glog "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/auth" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -269,7 +270,7 @@ func (proxy *DataSourceProxy) director(req *http.Request) { } } - if proxy.features.IsEnabled(featuremgmt.FlagIdForwarding) { + if proxy.features.IsEnabled(featuremgmt.FlagIdForwarding) && auth.IsIDForwardingEnabledForDataSource(proxy.ds) { proxyutil.ApplyForwardIDHeader(req, proxy.ctx.SignedInUser) } } diff --git a/pkg/services/auth/id.go b/pkg/services/auth/id.go index 33d505b23f7..4a0364db707 100644 --- a/pkg/services/auth/id.go +++ b/pkg/services/auth/id.go @@ -4,7 +4,9 @@ import ( "context" "github.com/go-jose/go-jose/v3/jwt" + "github.com/grafana/grafana/pkg/services/auth/identity" + "github.com/grafana/grafana/pkg/services/datasources" ) type IDService interface { @@ -19,3 +21,9 @@ type IDSigner interface { type IDClaims struct { jwt.Claims } + +const settingsKey = "forwardIdToken" + +func IsIDForwardingEnabledForDataSource(ds *datasources.DataSource) bool { + return ds.JsonData != nil && ds.JsonData.Get(settingsKey).MustBool() +} diff --git a/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware.go index 9e7f9269b3f..ee7cf30738c 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware.go @@ -4,8 +4,12 @@ import ( "context" "github.com/grafana/grafana-plugin-sdk-go/backend" + + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/contexthandler" + "github.com/grafana/grafana/pkg/services/datasources" ) const forwardIDHeaderName = "X-Grafana-Id" @@ -28,7 +32,16 @@ type ForwardIDMiddleware struct { func (m *ForwardIDMiddleware) applyToken(ctx context.Context, pCtx backend.PluginContext, req backend.ForwardHTTPHeaders) error { reqCtx := contexthandler.FromContext(ctx) // if request not for a datasource or no HTTP request context skip middleware - if req == nil || reqCtx == nil || reqCtx.SignedInUser == nil { + if req == nil || reqCtx == nil || reqCtx.SignedInUser == nil || pCtx.DataSourceInstanceSettings == nil { + return nil + } + + jsonDataBytes, err := simplejson.NewJson(pCtx.DataSourceInstanceSettings.JSONData) + if err != nil { + return err + } + + if !auth.IsIDForwardingEnabledForDataSource(&datasources.DataSource{JsonData: jsonDataBytes}) { return nil } diff --git a/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware_test.go index 6ca5773256e..087bf29729e 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware_test.go @@ -2,20 +2,31 @@ package clientmiddleware import ( "context" + "encoding/json" "net/http" "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/web" ) func TestForwardIDMiddleware(t *testing.T) { + settingWithEnabled, err := json.Marshal(map[string]any{ + "forwardIdToken": true, + }) + require.NoError(t, err) + + settingWithDisabled, err := json.Marshal(map[string]any{ + "forwardIdToken": false, + }) + require.NoError(t, err) + t.Run("Should set forwarded id header if present", func(t *testing.T) { cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewForwardIDMiddleware())) @@ -25,13 +36,36 @@ func TestForwardIDMiddleware(t *testing.T) { }) err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ - PluginContext: backend.PluginContext{}, + PluginContext: backend.PluginContext{ + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ + JSONData: settingWithEnabled, + }, + }, }, nopCallResourceSender) require.NoError(t, err) require.Equal(t, "some-token", cdt.CallResourceReq.Headers[forwardIDHeaderName][0]) }) + t.Run("Should not set forwarded id header if setting is disabled", func(t *testing.T) { + cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewForwardIDMiddleware())) + + ctx := context.WithValue(context.Background(), ctxkey.Key{}, &contextmodel.ReqContext{ + Context: &web.Context{Req: &http.Request{}}, + SignedInUser: &user.SignedInUser{IDToken: "some-token"}, + }) + + err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ + PluginContext: backend.PluginContext{ + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ + JSONData: settingWithDisabled, + }, + }, + }, nopCallResourceSender) + require.NoError(t, err) + require.Len(t, cdt.CallResourceReq.Headers[forwardIDHeaderName], 0) + }) + t.Run("Should not set forwarded id header if not present", func(t *testing.T) { cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewForwardIDMiddleware())) @@ -41,7 +75,11 @@ func TestForwardIDMiddleware(t *testing.T) { }) err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ - PluginContext: backend.PluginContext{}, + PluginContext: backend.PluginContext{ + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ + JSONData: settingWithEnabled, + }, + }, }, nopCallResourceSender) require.NoError(t, err) From aa9fc3be7299710334759125d0532f3c6818c467 Mon Sep 17 00:00:00 2001 From: Horst Gutmann Date: Fri, 27 Oct 2023 09:20:20 +0200 Subject: [PATCH 115/180] CI: Fix release-npm-packages action (#77127) * Remove dependency of NpmReleaseAction to the git binary * Switch to using the node image for the release-npm-packages step --- .drone.yml | 4 ++-- pkg/build/cmd/npm.go | 7 ------- scripts/drone/events/release.star | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/.drone.yml b/.drone.yml index d0814c24407..a6a34bff2fa 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2711,7 +2711,7 @@ steps: NPM_TOKEN: from_secret: npm_token failure: ignore - image: golang:1.20.10-alpine + image: node:20.9.0-alpine name: release-npm-packages trigger: event: @@ -4668,6 +4668,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 4c04040e7890d9f1da64ff3e2c006db1b3de4276b57ce4401849ec4ceb665ea8 +hmac: 2a51cf7ded5c749dc6b3664e5cadc6378910e772bf44ea8c80457b5ddb61e3a5 ... diff --git a/pkg/build/cmd/npm.go b/pkg/build/cmd/npm.go index 13994fb4e19..ba35679f57a 100644 --- a/pkg/build/cmd/npm.go +++ b/pkg/build/cmd/npm.go @@ -3,7 +3,6 @@ package main import ( "fmt" "os" - "os/exec" "strings" "github.com/urfave/cli/v2" @@ -74,12 +73,6 @@ func NpmReleaseAction(c *cli.Context) error { return fmt.Errorf("no tag version specified, exitting") } - cmd := exec.Command("git", "checkout", ".") - if err := cmd.Run(); err != nil { - fmt.Println("command failed to run, err: ", err) - return err - } - err := npm.PublishNpmPackages(c.Context, tag) if err != nil { return err diff --git a/scripts/drone/events/release.star b/scripts/drone/events/release.star index 5e71f1eed04..cdf8c9883c0 100644 --- a/scripts/drone/events/release.star +++ b/scripts/drone/events/release.star @@ -62,7 +62,7 @@ def retrieve_npm_packages_step(): def release_npm_packages_step(): return { "name": "release-npm-packages", - "image": images["go"], + "image": images["node"], "depends_on": [ "compile-build-cmd", "retrieve-npm-packages", From 45bcbff115984f7df20c2f67483beccef074068a Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Fri, 27 Oct 2023 10:14:07 +0200 Subject: [PATCH 116/180] Plugins: Fix plugin alias ID being used in PluginContext.ID (#77206) * Fix PluginID being populated with alias in plugincontext Get and GetWithDataSource * Add tests * pr review suggestion * pr review suggestion --- .../plugincontext/plugincontext.go | 4 +- .../plugincontext/plugincontext_test.go | 77 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 pkg/services/pluginsintegration/plugincontext/plugincontext_test.go diff --git a/pkg/services/pluginsintegration/plugincontext/plugincontext.go b/pkg/services/pluginsintegration/plugincontext/plugincontext.go index 924c1795ae9..c6513706166 100644 --- a/pkg/services/pluginsintegration/plugincontext/plugincontext.go +++ b/pkg/services/pluginsintegration/plugincontext/plugincontext.go @@ -64,7 +64,7 @@ func (p *Provider) Get(ctx context.Context, pluginID string, user identity.Reque } pCtx := backend.PluginContext{ - PluginID: pluginID, + PluginID: plugin.ID, PluginVersion: plugin.Info.Version, } if user != nil && !user.IsNil() { @@ -99,7 +99,7 @@ func (p *Provider) GetWithDataSource(ctx context.Context, pluginID string, user } pCtx := backend.PluginContext{ - PluginID: pluginID, + PluginID: plugin.ID, PluginVersion: plugin.Info.Version, } if user != nil && !user.IsNil() { diff --git a/pkg/services/pluginsintegration/plugincontext/plugincontext_test.go b/pkg/services/pluginsintegration/plugincontext/plugincontext_test.go new file mode 100644 index 00000000000..5e19a17a048 --- /dev/null +++ b/pkg/services/pluginsintegration/plugincontext/plugincontext_test.go @@ -0,0 +1,77 @@ +package plugincontext_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/db/dbtest" + "github.com/grafana/grafana/pkg/infra/localcache" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + pluginFakes "github.com/grafana/grafana/pkg/plugins/manager/fakes" + "github.com/grafana/grafana/pkg/plugins/manager/registry" + "github.com/grafana/grafana/pkg/services/datasources" + fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" + "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" + pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" + secretstest "github.com/grafana/grafana/pkg/services/secrets/fakes" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" +) + +func TestGet(t *testing.T) { + const ( + pluginID = "plugin-id" + alias = "alias" + ) + + preg := registry.NewInMemory() + require.NoError(t, preg.Add(context.Background(), &plugins.Plugin{ + JSONData: plugins.JSONData{ + ID: pluginID, + AliasIDs: []string{alias}, + }, + })) + + cfg := setting.NewCfg() + ds := &fakeDatasources.FakeDataSourceService{} + db := &dbtest.FakeDB{ExpectedError: pluginsettings.ErrPluginSettingNotFound} + pcp := plugincontext.ProvideService(cfg, localcache.ProvideService(), + pluginstore.New(preg, &pluginFakes.FakeLoader{}), + ds, pluginSettings.ProvideService(db, secretstest.NewFakeSecretsService()), pluginFakes.NewFakeLicensingService(), &config.Cfg{}, + ) + identity := &user.SignedInUser{OrgID: int64(1), Login: "admin"} + + for _, tc := range []struct { + name string + input string + }{ + {"with id", pluginID}, + {"with alias", alias}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Run("Get", func(t *testing.T) { + pCtx, err := pcp.Get(context.Background(), tc.input, identity, identity.OrgID) + require.NoError(t, err) + require.Equal(t, pluginID, pCtx.PluginID) + }) + + t.Run("GetWithDataSource", func(t *testing.T) { + pCtx, err := pcp.GetWithDataSource(context.Background(), tc.input, identity, &datasources.DataSource{ + ID: 1, + OrgID: 1, + Name: "test", + Type: pluginID, + JsonData: simplejson.New(), + }) + require.NoError(t, err) + require.Equal(t, pluginID, pCtx.PluginID) + }) + }) + } +} From bc9fab6f305d5225e5adac92f2b009d21e8af48e Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Fri, 27 Oct 2023 10:20:49 +0200 Subject: [PATCH 117/180] IDForwarding: Update settings name (#77257) Update settings name --- pkg/services/auth/id.go | 2 +- .../clientmiddleware/forward_id_middleware_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/auth/id.go b/pkg/services/auth/id.go index 4a0364db707..82aa7905a5b 100644 --- a/pkg/services/auth/id.go +++ b/pkg/services/auth/id.go @@ -22,7 +22,7 @@ type IDClaims struct { jwt.Claims } -const settingsKey = "forwardIdToken" +const settingsKey = "forwardGrafanaIdToken" func IsIDForwardingEnabledForDataSource(ds *datasources.DataSource) bool { return ds.JsonData != nil && ds.JsonData.Get(settingsKey).MustBool() diff --git a/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware_test.go index 087bf29729e..42c0953cad4 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware_test.go @@ -18,12 +18,12 @@ import ( func TestForwardIDMiddleware(t *testing.T) { settingWithEnabled, err := json.Marshal(map[string]any{ - "forwardIdToken": true, + "forwardGrafanaIdToken": true, }) require.NoError(t, err) settingWithDisabled, err := json.Marshal(map[string]any{ - "forwardIdToken": false, + "forwardGrafanaIdToken": false, }) require.NoError(t, err) From 49f8838b62e23ca710f320c6fb1b9224547a26ab Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 27 Oct 2023 09:26:38 +0100 Subject: [PATCH 118/180] Navigation: Improve docked auto scroll behaviour (#77117) ensure expand works on any location change, only scroll when docked --- .../AppChrome/DockedMegaMenu/MegaMenuItem.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx index 11fbe24bbc7..4801007f4b9 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx @@ -1,9 +1,11 @@ import { css, cx } from '@emotion/css'; import React, { useEffect, useRef } from 'react'; +import { useLocation } from 'react-router-dom'; import { useLocalStorage } from 'react-use'; import { GrafanaTheme2, NavModelItem, toIconName } from '@grafana/data'; import { useStyles2, Text, IconButton, Icon } from '@grafana/ui'; +import { useGrafana } from 'app/core/context/GrafanaContext'; import { Indent } from '../../Indent/Indent'; @@ -21,6 +23,10 @@ interface Props { const MAX_DEPTH = 2; export function MegaMenuItem({ link, activeItem, level = 0, onClick }: Props) { + const { chrome } = useGrafana(); + const state = chrome.useState(); + const menuIsDocked = state.megaMenu === 'docked'; + const location = useLocation(); const FeatureHighlightWrapper = link.highlightText ? FeatureHighlight : React.Fragment; const hasActiveChild = hasChildMatch(link, activeItem); const isActive = link === activeItem || (level === MAX_DEPTH && hasActiveChild); @@ -38,16 +44,16 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClick }: Props) { if (hasActiveChild) { setSectionExpanded(true); } - }, [hasActiveChild, setSectionExpanded]); + }, [hasActiveChild, location, menuIsDocked, setSectionExpanded]); // scroll active element into center if it's offscreen useEffect(() => { - if (isActive && item.current && isElementOffscreen(item.current)) { + if (menuIsDocked && isActive && item.current && isElementOffscreen(item.current)) { item.current.scrollIntoView({ block: 'center', }); } - }, [isActive]); + }, [isActive, menuIsDocked]); if (!link.url) { return null; From edd0e80ba0812fd522467376421dae06bff29e52 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Oct 2023 09:42:23 +0100 Subject: [PATCH 119/180] Update dependency rc-cascader to v3.20.0 (#77210) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 42 ++++++++++++++++++++++---------- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 09fddea7e47..a05323c7368 100644 --- a/package.json +++ b/package.json @@ -361,7 +361,7 @@ "prismjs": "1.29.0", "prop-types": "15.8.1", "pseudoizer": "^0.1.0", - "rc-cascader": "3.19.0", + "rc-cascader": "3.20.0", "rc-drawer": "6.5.2", "rc-slider": "10.3.1", "rc-time-picker": "3.7.3", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 3d782edcc6a..d04401940d5 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -81,7 +81,7 @@ "monaco-editor": "0.34.0", "ol": "7.4.0", "prismjs": "1.29.0", - "rc-cascader": "3.19.0", + "rc-cascader": "3.20.0", "rc-drawer": "6.5.2", "rc-slider": "10.3.1", "rc-time-picker": "^3.7.3", diff --git a/yarn.lock b/yarn.lock index f5b24299c09..e25b77f75d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3451,7 +3451,7 @@ __metadata: ol: 7.4.0 prismjs: 1.29.0 process: ^0.11.10 - rc-cascader: 3.19.0 + rc-cascader: 3.20.0 rc-drawer: 6.5.2 rc-slider: 10.3.1 rc-time-picker: ^3.7.3 @@ -18029,7 +18029,7 @@ __metadata: prismjs: 1.29.0 prop-types: 15.8.1 pseudoizer: ^0.1.0 - rc-cascader: 3.19.0 + rc-cascader: 3.20.0 rc-drawer: 6.5.2 rc-slider: 10.3.1 rc-time-picker: 3.7.3 @@ -24843,20 +24843,20 @@ __metadata: languageName: node linkType: hard -"rc-cascader@npm:3.19.0": - version: 3.19.0 - resolution: "rc-cascader@npm:3.19.0" +"rc-cascader@npm:3.20.0": + version: 3.20.0 + resolution: "rc-cascader@npm:3.20.0" dependencies: "@babel/runtime": ^7.12.5 array-tree-filter: ^2.1.0 classnames: ^2.3.1 - rc-select: ~14.9.0 - rc-tree: ~5.8.0 + rc-select: ~14.10.0 + rc-tree: ~5.8.1 rc-util: ^5.37.0 peerDependencies: react: ">=16.9.0" react-dom: ">=16.9.0" - checksum: d4afadd601f652a9ce1679ea0f069eb53bed608ba8b89dab7d815faee65cbb47f45325378f36941e3d30424ea407dd48d79db6b06ca8f1f258856b0471e0bcec + checksum: fd85091f90c7a82ff8e240c356de9f1070e6371217a7ab852908b64746488586d8c9b2893ce5895373e1e8d55c36d5cd899808ec6d7938bfe81d19be2ceee94a languageName: node linkType: hard @@ -24920,9 +24920,9 @@ __metadata: languageName: node linkType: hard -"rc-select@npm:~14.9.0": - version: 14.9.1 - resolution: "rc-select@npm:14.9.1" +"rc-select@npm:~14.10.0": + version: 14.10.0 + resolution: "rc-select@npm:14.10.0" dependencies: "@babel/runtime": ^7.10.1 "@rc-component/trigger": ^1.5.0 @@ -24934,7 +24934,7 @@ __metadata: peerDependencies: react: "*" react-dom: "*" - checksum: 914e6ae273c6916e321710e6eaa69d3a5a50ce2a36fc91c22bd1c42d95cd8083d6f10fe0c2324f07829c60b9ad30b481910703d11638885e6ee29429d4c4f8e5 + checksum: 1f922000e64338b7c43ba0e67429e482291f4e8d9e2d1977e0414171ff388050de4802c780baaa4e48c299b025c2334227382d3c47ca1f2888dbef83c73ab43e languageName: node linkType: hard @@ -24980,7 +24980,7 @@ __metadata: languageName: node linkType: hard -"rc-tree@npm:5.8.0, rc-tree@npm:~5.8.0": +"rc-tree@npm:5.8.0": version: 5.8.0 resolution: "rc-tree@npm:5.8.0" dependencies: @@ -24996,6 +24996,22 @@ __metadata: languageName: node linkType: hard +"rc-tree@npm:~5.8.1": + version: 5.8.2 + resolution: "rc-tree@npm:5.8.2" + dependencies: + "@babel/runtime": ^7.10.1 + classnames: 2.x + rc-motion: ^2.0.1 + rc-util: ^5.16.1 + rc-virtual-list: ^3.5.1 + peerDependencies: + react: "*" + react-dom: "*" + checksum: 74802b2e670fd6696e294ba6eeb20381feab5704e8f92de981725e56b00070c87ef0c2ece2846566715ee7420878743cd22d3443235732282400b6e475ecff36 + languageName: node + linkType: hard + "rc-trigger@npm:^2.2.0": version: 2.6.5 resolution: "rc-trigger@npm:2.6.5" From df413784729179932dd3267f99254ace6eec89dc Mon Sep 17 00:00:00 2001 From: Shabeeb Khalid Date: Fri, 27 Oct 2023 11:49:37 +0300 Subject: [PATCH 120/180] CloudWatch: Use context in aws ListSinks and ListAttachedLinks (#77083) * Use context in aws ListSinks and ListAttachedLinks In the current way, ListSinks and ListAttachedLinks is used which doesn't allow cancelling the request if the context changes. Using ListSinksWithContext and ListAttachedLinksWithContext is the preferred way. Adding context for GetAccountsForCurrentUserOrRole is required to pass it to ListSinks method. --- pkg/tsdb/cloudwatch/mocks/accounts_service.go | 4 +- pkg/tsdb/cloudwatch/mocks/oam_client.go | 7 ++- pkg/tsdb/cloudwatch/models/api.go | 6 +-- pkg/tsdb/cloudwatch/routes/accounts.go | 2 +- pkg/tsdb/cloudwatch/services/accounts.go | 7 +-- pkg/tsdb/cloudwatch/services/accounts_test.go | 53 ++++++++++--------- 6 files changed, 43 insertions(+), 36 deletions(-) diff --git a/pkg/tsdb/cloudwatch/mocks/accounts_service.go b/pkg/tsdb/cloudwatch/mocks/accounts_service.go index 52f3aff63b5..d7b742d6848 100644 --- a/pkg/tsdb/cloudwatch/mocks/accounts_service.go +++ b/pkg/tsdb/cloudwatch/mocks/accounts_service.go @@ -1,6 +1,8 @@ package mocks import ( + "context" + "github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources" "github.com/stretchr/testify/mock" ) @@ -9,7 +11,7 @@ type AccountsServiceMock struct { mock.Mock } -func (a *AccountsServiceMock) GetAccountsForCurrentUserOrRole() ([]resources.ResourceResponse[resources.Account], error) { +func (a *AccountsServiceMock) GetAccountsForCurrentUserOrRole(ctx context.Context) ([]resources.ResourceResponse[resources.Account], error) { args := a.Called() return args.Get(0).([]resources.ResourceResponse[resources.Account]), args.Error(1) diff --git a/pkg/tsdb/cloudwatch/mocks/oam_client.go b/pkg/tsdb/cloudwatch/mocks/oam_client.go index 2d617cc143a..932ca1579b4 100644 --- a/pkg/tsdb/cloudwatch/mocks/oam_client.go +++ b/pkg/tsdb/cloudwatch/mocks/oam_client.go @@ -1,6 +1,9 @@ package mocks import ( + "context" + + "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/service/oam" "github.com/stretchr/testify/mock" ) @@ -9,12 +12,12 @@ type FakeOAMClient struct { mock.Mock } -func (o *FakeOAMClient) ListSinks(input *oam.ListSinksInput) (*oam.ListSinksOutput, error) { +func (o *FakeOAMClient) ListSinksWithContext(ctx context.Context, input *oam.ListSinksInput, opts ...request.Option) (*oam.ListSinksOutput, error) { args := o.Called(input) return args.Get(0).(*oam.ListSinksOutput), args.Error(1) } -func (o *FakeOAMClient) ListAttachedLinks(input *oam.ListAttachedLinksInput) (*oam.ListAttachedLinksOutput, error) { +func (o *FakeOAMClient) ListAttachedLinksWithContext(ctx context.Context, input *oam.ListAttachedLinksInput, opts ...request.Option) (*oam.ListAttachedLinksOutput, error) { args := o.Called(input) return args.Get(0).(*oam.ListAttachedLinksOutput), args.Error(1) } diff --git a/pkg/tsdb/cloudwatch/models/api.go b/pkg/tsdb/cloudwatch/models/api.go index 42d54c84e81..dc540f54d6f 100644 --- a/pkg/tsdb/cloudwatch/models/api.go +++ b/pkg/tsdb/cloudwatch/models/api.go @@ -42,7 +42,7 @@ type LogGroupsProvider interface { } type AccountsProvider interface { - GetAccountsForCurrentUserOrRole() ([]resources.ResourceResponse[resources.Account], error) + GetAccountsForCurrentUserOrRole(ctx context.Context) ([]resources.ResourceResponse[resources.Account], error) } type RegionsAPIProvider interface { @@ -65,8 +65,8 @@ type CloudWatchLogsAPIProvider interface { } type OAMAPIProvider interface { - ListSinks(*oam.ListSinksInput) (*oam.ListSinksOutput, error) - ListAttachedLinks(*oam.ListAttachedLinksInput) (*oam.ListAttachedLinksOutput, error) + ListSinksWithContext(ctx context.Context, in *oam.ListSinksInput, opts ...request.Option) (*oam.ListSinksOutput, error) + ListAttachedLinksWithContext(ctx context.Context, in *oam.ListAttachedLinksInput, opts ...request.Option) (*oam.ListAttachedLinksOutput, error) } type EC2APIProvider interface { diff --git a/pkg/tsdb/cloudwatch/routes/accounts.go b/pkg/tsdb/cloudwatch/routes/accounts.go index 3c18591b55f..565a8c67ac4 100644 --- a/pkg/tsdb/cloudwatch/routes/accounts.go +++ b/pkg/tsdb/cloudwatch/routes/accounts.go @@ -24,7 +24,7 @@ func AccountsHandler(ctx context.Context, pluginCtx backend.PluginContext, reqCt return nil, models.NewHttpError("error in AccountsHandler", http.StatusInternalServerError, err) } - accounts, err := service.GetAccountsForCurrentUserOrRole() + accounts, err := service.GetAccountsForCurrentUserOrRole(ctx) if err != nil { msg := "error getting accounts for current user or role" switch { diff --git a/pkg/tsdb/cloudwatch/services/accounts.go b/pkg/tsdb/cloudwatch/services/accounts.go index 7f30f038050..f05b18335a3 100644 --- a/pkg/tsdb/cloudwatch/services/accounts.go +++ b/pkg/tsdb/cloudwatch/services/accounts.go @@ -1,6 +1,7 @@ package services import ( + "context" "errors" "fmt" @@ -20,11 +21,11 @@ func NewAccountsService(oamClient models.OAMAPIProvider) models.AccountsProvider return &AccountsService{oamClient} } -func (a *AccountsService) GetAccountsForCurrentUserOrRole() ([]resources.ResourceResponse[resources.Account], error) { +func (a *AccountsService) GetAccountsForCurrentUserOrRole(ctx context.Context) ([]resources.ResourceResponse[resources.Account], error) { var nextToken *string sinks := []*oam.ListSinksItem{} for { - response, err := a.ListSinks(&oam.ListSinksInput{NextToken: nextToken}) + response, err := a.ListSinksWithContext(ctx, &oam.ListSinksInput{NextToken: nextToken}) if err != nil { var aerr awserr.Error if errors.As(err, &aerr) { @@ -61,7 +62,7 @@ func (a *AccountsService) GetAccountsForCurrentUserOrRole() ([]resources.Resourc nextToken = nil for { - links, err := a.ListAttachedLinks(&oam.ListAttachedLinksInput{ + links, err := a.ListAttachedLinksWithContext(ctx, &oam.ListAttachedLinksInput{ SinkIdentifier: sinkIdentifier, NextToken: nextToken, }) diff --git a/pkg/tsdb/cloudwatch/services/accounts_test.go b/pkg/tsdb/cloudwatch/services/accounts_test.go index 7e74e6fbec9..dbbadaf660c 100644 --- a/pkg/tsdb/cloudwatch/services/accounts_test.go +++ b/pkg/tsdb/cloudwatch/services/accounts_test.go @@ -1,6 +1,7 @@ package services import ( + "context" "fmt" "testing" @@ -17,11 +18,11 @@ import ( func TestHandleGetAccounts(t *testing.T) { t.Run("Should return an error in case of insufficient permissions from ListSinks", func(t *testing.T) { fakeOAMClient := &mocks.FakeOAMClient{} - fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{}, awserr.New("AccessDeniedException", + fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{}, awserr.New("AccessDeniedException", "AWS message", nil)) accounts := NewAccountsService(fakeOAMClient) - resp, err := accounts.GetAccountsForCurrentUserOrRole() + resp, err := accounts.GetAccountsForCurrentUserOrRole(context.Background()) assert.Error(t, err) assert.Nil(t, resp) @@ -31,10 +32,10 @@ func TestHandleGetAccounts(t *testing.T) { t.Run("Should return an error in case of any error from ListSinks", func(t *testing.T) { fakeOAMClient := &mocks.FakeOAMClient{} - fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{}, fmt.Errorf("some error")) + fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{}, fmt.Errorf("some error")) accounts := NewAccountsService(fakeOAMClient) - resp, err := accounts.GetAccountsForCurrentUserOrRole() + resp, err := accounts.GetAccountsForCurrentUserOrRole(context.Background()) assert.Error(t, err) assert.Nil(t, resp) @@ -43,10 +44,10 @@ func TestHandleGetAccounts(t *testing.T) { t.Run("Should return empty array in case no monitoring account exists", func(t *testing.T) { fakeOAMClient := &mocks.FakeOAMClient{} - fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{}, nil) + fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{}, nil) accounts := NewAccountsService(fakeOAMClient) - resp, err := accounts.GetAccountsForCurrentUserOrRole() + resp, err := accounts.GetAccountsForCurrentUserOrRole(context.Background()) assert.NoError(t, err) assert.Empty(t, resp) @@ -54,26 +55,26 @@ func TestHandleGetAccounts(t *testing.T) { t.Run("Should return one monitoring account (the first) even though ListSinks returns multiple sinks", func(t *testing.T) { fakeOAMClient := &mocks.FakeOAMClient{} - fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{ + fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{ Items: []*oam.ListSinksItem{ {Name: aws.String("Account 1"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group1")}, {Name: aws.String("Account 2"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group2")}, }, NextToken: new(string), }, nil).Once() - fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{ + fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{ Items: []*oam.ListSinksItem{ {Name: aws.String("Account 3"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group3")}, }, NextToken: nil, }, nil) - fakeOAMClient.On("ListAttachedLinks", mock.Anything).Return(&oam.ListAttachedLinksOutput{}, nil) + fakeOAMClient.On("ListAttachedLinksWithContext", mock.Anything).Return(&oam.ListAttachedLinksOutput{}, nil) accounts := NewAccountsService(fakeOAMClient) - resp, err := accounts.GetAccountsForCurrentUserOrRole() + resp, err := accounts.GetAccountsForCurrentUserOrRole(context.Background()) assert.NoError(t, err) - fakeOAMClient.AssertNumberOfCalls(t, "ListSinks", 2) + fakeOAMClient.AssertNumberOfCalls(t, "ListSinksWithContext", 2) require.Len(t, resp, 1) assert.True(t, resp[0].Value.IsMonitoringAccount) assert.Equal(t, "Account 1", resp[0].Value.Label) @@ -82,27 +83,27 @@ func TestHandleGetAccounts(t *testing.T) { t.Run("Should merge the first sink with attached links", func(t *testing.T) { fakeOAMClient := &mocks.FakeOAMClient{} - fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{ + fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{ Items: []*oam.ListSinksItem{ {Name: aws.String("Account 1"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group1")}, {Name: aws.String("Account 2"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group2")}, }, NextToken: new(string), }, nil).Once() - fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{ + fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{ Items: []*oam.ListSinksItem{ {Name: aws.String("Account 3"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group3")}, }, NextToken: nil, }, nil) - fakeOAMClient.On("ListAttachedLinks", mock.Anything).Return(&oam.ListAttachedLinksOutput{ + fakeOAMClient.On("ListAttachedLinksWithContext", mock.Anything).Return(&oam.ListAttachedLinksOutput{ Items: []*oam.ListAttachedLinksItem{ {Label: aws.String("Account 10"), LinkArn: aws.String("arn:aws:logs:us-east-1:123456789013:log-group:my-log-group10")}, {Label: aws.String("Account 11"), LinkArn: aws.String("arn:aws:logs:us-east-1:123456789014:log-group:my-log-group11")}, }, NextToken: new(string), }, nil).Once() - fakeOAMClient.On("ListAttachedLinks", mock.Anything).Return(&oam.ListAttachedLinksOutput{ + fakeOAMClient.On("ListAttachedLinksWithContext", mock.Anything).Return(&oam.ListAttachedLinksOutput{ Items: []*oam.ListAttachedLinksItem{ {Label: aws.String("Account 12"), LinkArn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group12")}, }, @@ -110,11 +111,11 @@ func TestHandleGetAccounts(t *testing.T) { }, nil) accounts := NewAccountsService(fakeOAMClient) - resp, err := accounts.GetAccountsForCurrentUserOrRole() + resp, err := accounts.GetAccountsForCurrentUserOrRole(context.Background()) assert.NoError(t, err) - fakeOAMClient.AssertNumberOfCalls(t, "ListSinks", 2) - fakeOAMClient.AssertNumberOfCalls(t, "ListAttachedLinks", 2) + fakeOAMClient.AssertNumberOfCalls(t, "ListSinksWithContext", 2) + fakeOAMClient.AssertNumberOfCalls(t, "ListAttachedLinksWithContext", 2) expectedAccounts := []resources.ResourceResponse[resources.Account]{ {Value: resources.Account{Id: "123456789012", Label: "Account 1", Arn: "arn:aws:logs:us-east-1:123456789012:log-group:my-log-group1", IsMonitoringAccount: true}}, {Value: resources.Account{Id: "123456789013", Label: "Account 10", Arn: "arn:aws:logs:us-east-1:123456789013:log-group:my-log-group10", IsMonitoringAccount: false}}, @@ -126,37 +127,37 @@ func TestHandleGetAccounts(t *testing.T) { t.Run("Should call ListAttachedLinks with arn of first sink", func(t *testing.T) { fakeOAMClient := &mocks.FakeOAMClient{} - fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{ + fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{ Items: []*oam.ListSinksItem{ {Name: aws.String("Account 1"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group1")}, }, NextToken: new(string), }, nil).Once() - fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{ + fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{ Items: []*oam.ListSinksItem{ {Name: aws.String("Account 3"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group3")}, }, NextToken: nil, }, nil).Once() - fakeOAMClient.On("ListAttachedLinks", mock.Anything).Return(&oam.ListAttachedLinksOutput{}, nil) + fakeOAMClient.On("ListAttachedLinksWithContext", mock.Anything).Return(&oam.ListAttachedLinksOutput{}, nil) accounts := NewAccountsService(fakeOAMClient) - _, _ = accounts.GetAccountsForCurrentUserOrRole() + _, _ = accounts.GetAccountsForCurrentUserOrRole(context.Background()) - fakeOAMClient.AssertCalled(t, "ListAttachedLinks", &oam.ListAttachedLinksInput{ + fakeOAMClient.AssertCalled(t, "ListAttachedLinksWithContext", &oam.ListAttachedLinksInput{ SinkIdentifier: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group1"), }) }) t.Run("Should return an error in case of any error from ListAttachedLinks", func(t *testing.T) { fakeOAMClient := &mocks.FakeOAMClient{} - fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{ + fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{ Items: []*oam.ListSinksItem{{Name: aws.String("Account 1"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group1")}}, }, nil) - fakeOAMClient.On("ListAttachedLinks", mock.Anything).Return(&oam.ListAttachedLinksOutput{}, fmt.Errorf("some error")).Once() + fakeOAMClient.On("ListAttachedLinksWithContext", mock.Anything).Return(&oam.ListAttachedLinksOutput{}, fmt.Errorf("some error")).Once() accounts := NewAccountsService(fakeOAMClient) - resp, err := accounts.GetAccountsForCurrentUserOrRole() + resp, err := accounts.GetAccountsForCurrentUserOrRole(context.Background()) assert.Error(t, err) assert.Nil(t, resp) From 5d706705eae40b94191b4937a5cafc23d1efe615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 27 Oct 2023 11:35:06 +0200 Subject: [PATCH 121/180] RadioButtonGroup: Fixes icon alignment (#77196) --- .../src/components/Forms/RadioButtonGroup/RadioButton.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx index 8a9b7a1998f..9eb267e637f 100644 --- a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx +++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx @@ -108,6 +108,8 @@ const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme2, size: RadioBut }, }), radioLabel: css({ + display: 'flex', + alignItems: 'center', fontSize, height: `${labelHeight}px`, // Deduct border from line-height for perfect vertical centering on windows and linux From 9ad26e4f3947c7c90e3781bfc7838fc3cf39c255 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Oct 2023 11:33:13 +0100 Subject: [PATCH 122/180] Update dependency @grafana/scenes to v1.20.0 (#77261) * Update dependency @grafana/scenes to v1.20.0 * update e2e test --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ashley Harrison --- e2e/dashboards-suite/dashboard-templating.spec.ts | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e/dashboards-suite/dashboard-templating.spec.ts b/e2e/dashboards-suite/dashboard-templating.spec.ts index 7fa46d86ea3..71b9fcbf7db 100644 --- a/e2e/dashboards-suite/dashboard-templating.spec.ts +++ b/e2e/dashboards-suite/dashboard-templating.spec.ts @@ -31,7 +31,7 @@ describe('Dashboard templating', () => { `Server:percentencode = %7BA%27A%22A%2CBB%5CB%2CCCC%7D`, `Server:singlequote = 'A\\'A"A','BB\\B','CCC'`, `Server:doublequote = "A'A\\"A","BB\\B","CCC"`, - `Server:sqlstring = 'A''A"A','BB\\\B','CCC'`, + `Server:sqlstring = 'A''A\\"A','BB\\\B','CCC'`, `Server:date = NaN`, `Server:text = All`, `Server:queryparam = var-Server=All`, diff --git a/yarn.lock b/yarn.lock index e25b77f75d2..69a564b1b9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3305,8 +3305,8 @@ __metadata: linkType: soft "@grafana/scenes@npm:^1.18.0": - version: 1.19.0 - resolution: "@grafana/scenes@npm:1.19.0" + version: 1.20.0 + resolution: "@grafana/scenes@npm:1.20.0" dependencies: "@grafana/e2e-selectors": 10.0.2 react-grid-layout: 1.3.4 @@ -3318,7 +3318,7 @@ __metadata: "@grafana/runtime": 10.0.3 "@grafana/schema": 10.0.3 "@grafana/ui": 10.0.3 - checksum: ce6f7db1126417c7516f0d72c9fd327f7952fdef0da14951798088e1d30498033e47cb988abcd90d72db9c9882328e1804fd6ec0be68bc021c5830b122e5b575 + checksum: cc56fa2aec1e31598a9cbb7f4837d25fc03f32879247fc2063269c1d9c8d8543c518f6cac3c4b64ea32b4f0841e334bd1af4bd38b45860c0a89473680385cafd languageName: node linkType: hard From 8ba6dc866d93bab8fa13a06effae54f816048e4b Mon Sep 17 00:00:00 2001 From: Daniel Benjamin Date: Fri, 27 Oct 2023 11:41:04 +0100 Subject: [PATCH 123/180] chore: remove LegacyForms from MetricSelect component (#76490) --- public/app/core/components/Select/MetricSelect.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/public/app/core/components/Select/MetricSelect.tsx b/public/app/core/components/Select/MetricSelect.tsx index cdbbc7be4a2..c1fca990418 100644 --- a/public/app/core/components/Select/MetricSelect.tsx +++ b/public/app/core/components/Select/MetricSelect.tsx @@ -1,10 +1,9 @@ import { flatten } from 'lodash'; -import React, { useMemo, useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; -import { LegacyForms } from '@grafana/ui'; +import { Select } from '@grafana/ui'; import { Variable } from 'app/types/templates'; -const { Select } = LegacyForms; export interface Props { onChange: (value: string | undefined) => void; @@ -33,7 +32,7 @@ export const MetricSelect = (props: Props) => { isSearchable={isSearchable} maxMenuHeight={500} placeholder={placeholder} - noOptionsMessage={() => 'No options found'} + noOptionsMessage="No options found" value={selected} /> ); From bd6dae63be6776156c8940a6f2aaa8f8ee0ccaf7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Oct 2023 11:56:51 +0100 Subject: [PATCH 124/180] Update dependency eslint-plugin-jest to v27.6.0 (#77267) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a05323c7368..a16b892c637 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,7 @@ "eslint": "8.52.0", "eslint-config-prettier": "8.8.0", "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jest": "27.4.2", + "eslint-plugin-jest": "27.6.0", "eslint-plugin-jsdoc": "46.8.2", "eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-lodash": "7.4.0", diff --git a/yarn.lock b/yarn.lock index 69a564b1b9c..2acb7d8eab2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15872,9 +15872,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jest@npm:27.4.2": - version: 27.4.2 - resolution: "eslint-plugin-jest@npm:27.4.2" +"eslint-plugin-jest@npm:27.6.0": + version: 27.6.0 + resolution: "eslint-plugin-jest@npm:27.6.0" dependencies: "@typescript-eslint/utils": ^5.10.0 peerDependencies: @@ -15886,7 +15886,7 @@ __metadata: optional: true jest: optional: true - checksum: 99a8301ae00c37da97866b8b13c89a077716d2c653b26bc417d242e7300a43237c0017fd488c43966fa38585f19050facdbbc71d03ca36a1ce6f2ba930a9143e + checksum: 4c42641f9bf2d597761637028083e20b9f81762308e98baae40eb805d3e81ff8d837f06f4f0c1a2fd249e2be2fb24d33b7aafeaa8942de805c2b8d7c3b6fc4e4 languageName: node linkType: hard @@ -17957,7 +17957,7 @@ __metadata: eslint: 8.52.0 eslint-config-prettier: 8.8.0 eslint-plugin-import: ^2.26.0 - eslint-plugin-jest: 27.4.2 + eslint-plugin-jest: 27.6.0 eslint-plugin-jsdoc: 46.8.2 eslint-plugin-jsx-a11y: 6.7.1 eslint-plugin-lodash: 7.4.0 From 8e8731edc5f7f2fd578b47ef42028ef8da1a64f6 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Fri, 27 Oct 2023 12:57:09 +0200 Subject: [PATCH 125/180] Chore: Bump Lerna to v7 (again) (#77190) * chore(lerna): update to latest version 7.4.1 and run lerna repair to update configs * Wip * chore(lerna): remove lerna specific packages field which prevents versioning packages * chore: remove nx from dependencies * chore(yarn): refresh lock file --- lerna.json | 2 - package.json | 2 +- yarn.lock | 2765 +++++++++++++++++++++----------------------------- 3 files changed, 1150 insertions(+), 1619 deletions(-) diff --git a/lerna.json b/lerna.json index 5f419bdcc36..636d8d0046a 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,4 @@ { "npmClient": "yarn", - "useWorkspaces": true, - "packages": ["packages/*"], "version": "10.3.0-pre" } diff --git a/package.json b/package.json index a16b892c637..b07ce3a1b76 100644 --- a/package.json +++ b/package.json @@ -198,7 +198,7 @@ "jest-fail-on-console": "3.1.1", "jest-junit": "16.0.0", "jest-matcher-utils": "29.7.0", - "lerna": "5.5.4", + "lerna": "7.4.1", "mini-css-extract-plugin": "2.7.6", "msw": "1.3.2", "mutationobserver-shim": "0.3.7", diff --git a/yarn.lock b/yarn.lock index 2acb7d8eab2..8c65fabf306 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3616,13 +3616,6 @@ __metadata: languageName: node linkType: hard -"@isaacs/string-locale-compare@npm:^1.1.0": - version: 1.1.0 - resolution: "@isaacs/string-locale-compare@npm:1.1.0" - checksum: 7287da5d11497b82c542d3c2abe534808015be4f4883e71c26853277b5456f6bbe4108535db847a29f385ad6dc9318ffb0f55ee79bb5f39993233d7dccf8751d - languageName: node - linkType: hard - "@istanbuljs/load-nyc-config@npm:^1.0.0": version: 1.1.0 resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" @@ -4033,807 +4026,87 @@ __metadata: languageName: node linkType: hard -"@lerna/add@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/add@npm:5.5.4" - dependencies: - "@lerna/bootstrap": 5.5.4 - "@lerna/command": 5.5.4 - "@lerna/filter-options": 5.5.4 - "@lerna/npm-conf": 5.5.4 - "@lerna/validation-error": 5.5.4 - dedent: ^0.7.0 - npm-package-arg: 8.1.1 - p-map: ^4.0.0 - pacote: ^13.6.1 - semver: ^7.3.4 - checksum: f4f17fda326a550cdbb3025a98a5ccf0e275b378fb1a1df6f518b5cbd3f334d5486394f84d12adddc8341d2802a37715390fdbf71375327dc89bdcd4986ef364 - languageName: node - linkType: hard - -"@lerna/bootstrap@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/bootstrap@npm:5.5.4" - dependencies: - "@lerna/command": 5.5.4 - "@lerna/filter-options": 5.5.4 - "@lerna/has-npm-version": 5.5.4 - "@lerna/npm-install": 5.5.4 - "@lerna/package-graph": 5.5.4 - "@lerna/pulse-till-done": 5.5.4 - "@lerna/rimraf-dir": 5.5.4 - "@lerna/run-lifecycle": 5.5.4 - "@lerna/run-topologically": 5.5.4 - "@lerna/symlink-binary": 5.5.4 - "@lerna/symlink-dependencies": 5.5.4 - "@lerna/validation-error": 5.5.4 - "@npmcli/arborist": 5.3.0 - dedent: ^0.7.0 - get-port: ^5.1.1 - multimatch: ^5.0.0 - npm-package-arg: 8.1.1 - npmlog: ^6.0.2 - p-map: ^4.0.0 - p-map-series: ^2.1.0 - p-waterfall: ^2.1.1 - semver: ^7.3.4 - checksum: 67a5f30045690b2b62be901c0272f6a67d830ca7183f1296d1b551a1d25c4e13ed0c1e6286a682685e42e2b6d5cd421dd16e812c12cefc43dd62036376fe4230 - languageName: node - linkType: hard - -"@lerna/changed@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/changed@npm:5.5.4" - dependencies: - "@lerna/collect-updates": 5.5.4 - "@lerna/command": 5.5.4 - "@lerna/listable": 5.5.4 - "@lerna/output": 5.5.4 - checksum: f2ff13b2a00740832428cfc626ae559c1cd210e8a6e71ee489c6872942c62cdec252598180bb3bada96d4ef6bc3ae494b5bb83b1b03e5313df96c52b4daf5e0d - languageName: node - linkType: hard - -"@lerna/check-working-tree@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/check-working-tree@npm:5.5.4" - dependencies: - "@lerna/collect-uncommitted": 5.5.4 - "@lerna/describe-ref": 5.5.4 - "@lerna/validation-error": 5.5.4 - checksum: 43d28c714b96ddf6d7cd9023f0f24a32786420d40746037a3cf4e35d03441ba4c3760826034034692ac355091d2a8a5166ee5538833762db51301982f732abaa - languageName: node - linkType: hard - -"@lerna/child-process@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/child-process@npm:5.5.4" +"@lerna/child-process@npm:7.4.1": + version: 7.4.1 + resolution: "@lerna/child-process@npm:7.4.1" dependencies: chalk: ^4.1.0 execa: ^5.0.0 strong-log-transformer: ^2.1.0 - checksum: f481252bd3aa2b1dc61fedf527840e5acf19e893ad15e3589ffa2466110d11f595b86402197cbc03fe8286339d8da1076f3bb25ef3d618a7aa4d18417a63e7e7 + checksum: 6be434a3d8aaf41e290dd0169133417cdb3b33ffd59fe77c7a927f28302fb8712a0be63fd261cf1b9c601000ed4dba1f86f8c0a8c3fa97fc665cd4e3458fc1ba languageName: node linkType: hard -"@lerna/clean@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/clean@npm:5.5.4" - dependencies: - "@lerna/command": 5.5.4 - "@lerna/filter-options": 5.5.4 - "@lerna/prompt": 5.5.4 - "@lerna/pulse-till-done": 5.5.4 - "@lerna/rimraf-dir": 5.5.4 - p-map: ^4.0.0 - p-map-series: ^2.1.0 - p-waterfall: ^2.1.1 - checksum: cf2aadf90f825cf5d458ba4dd4e4182e40983b23b6b3cd6ffc7ff9780b02f7552ca4106007e2af080728470a6e935ec1ba0a925b21d806fce1eb290838e0ee06 - languageName: node - linkType: hard - -"@lerna/cli@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/cli@npm:5.5.4" - dependencies: - "@lerna/global-options": 5.5.4 - dedent: ^0.7.0 - npmlog: ^6.0.2 - yargs: ^16.2.0 - checksum: 54f4106233550c98fabd3771d4f813f2e0284a6d8e71f8fd7105fcade317cc46016529441af0042dd38c713a35d4f6c992fa0add51025667b729c674f630a2da - languageName: node - linkType: hard - -"@lerna/collect-uncommitted@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/collect-uncommitted@npm:5.5.4" - dependencies: - "@lerna/child-process": 5.5.4 - chalk: ^4.1.0 - npmlog: ^6.0.2 - checksum: 3d0c1a9526651499799df689974f9e8efb4a3aac760f421ae18013cadd53c62eda4d1d3dbd152ceb8c864096b71ade01aeab5335e461495a12159a7cb33119d9 - languageName: node - linkType: hard - -"@lerna/collect-updates@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/collect-updates@npm:5.5.4" - dependencies: - "@lerna/child-process": 5.5.4 - "@lerna/describe-ref": 5.5.4 - minimatch: ^3.0.4 - npmlog: ^6.0.2 - slash: ^3.0.0 - checksum: dc051fdd205099dd005520549e50bf0c94a318c7d0e6ab51246e2278525c66dc98f513d3bbde7b33a9e9dd9d6d6d811666527570a56c0c9cadedca32db156969 - languageName: node - linkType: hard - -"@lerna/command@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/command@npm:5.5.4" - dependencies: - "@lerna/child-process": 5.5.4 - "@lerna/package-graph": 5.5.4 - "@lerna/project": 5.5.4 - "@lerna/validation-error": 5.5.4 - "@lerna/write-log-file": 5.5.4 - clone-deep: ^4.0.1 - dedent: ^0.7.0 - execa: ^5.0.0 - is-ci: ^2.0.0 - npmlog: ^6.0.2 - checksum: 096aadc9e3c0c0dc9f6127af9f655058360658ca73ffea476998a89594e29c6492894311ee7021df7532273b657c930d1060917b488ffccdff7643fa65bbeb25 - languageName: node - linkType: hard - -"@lerna/conventional-commits@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/conventional-commits@npm:5.5.4" - dependencies: - "@lerna/validation-error": 5.5.4 - conventional-changelog-angular: ^5.0.12 - conventional-changelog-core: ^4.2.4 - conventional-recommended-bump: ^6.1.0 - fs-extra: ^9.1.0 - get-stream: ^6.0.0 - npm-package-arg: 8.1.1 - npmlog: ^6.0.2 - pify: ^5.0.0 - semver: ^7.3.4 - checksum: 866731a1e1ff2bcb9795a10f6b462935f3e98bf353f24b7f04deea31c58a128e99ca9aabab24a6eb2181ea5f3f2e645d437bb485750cd2876853d48ec78211c0 - languageName: node - linkType: hard - -"@lerna/create-symlink@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/create-symlink@npm:5.5.4" - dependencies: - cmd-shim: ^5.0.0 - fs-extra: ^9.1.0 - npmlog: ^6.0.2 - checksum: 05c1bc24f450fc74b38991fd6a7f8a6df83b6777fe9456e1a0875071b032289942cba9d43173caef19df8946ef9eac7d423ed8c0fcb78980ddc6f24884298990 - languageName: node - linkType: hard - -"@lerna/create@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/create@npm:5.5.4" - dependencies: - "@lerna/child-process": 5.5.4 - "@lerna/command": 5.5.4 - "@lerna/npm-conf": 5.5.4 - "@lerna/validation-error": 5.5.4 - dedent: ^0.7.0 - fs-extra: ^9.1.0 - globby: ^11.0.2 - init-package-json: ^3.0.2 - npm-package-arg: 8.1.1 - p-reduce: ^2.1.0 - pacote: ^13.6.1 - pify: ^5.0.0 - semver: ^7.3.4 - slash: ^3.0.0 - validate-npm-package-license: ^3.0.4 - validate-npm-package-name: ^4.0.0 - yargs-parser: 20.2.4 - checksum: 44e63b3ea4cae77abd0fbd1fd5227e6d5691a67f48e8bcf53bb657a6014230ed3baf6584982a1c25811c47d751563e59850224dd3291c61ca30a7fb8ef69eff7 - languageName: node - linkType: hard - -"@lerna/describe-ref@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/describe-ref@npm:5.5.4" - dependencies: - "@lerna/child-process": 5.5.4 - npmlog: ^6.0.2 - checksum: 2ba2d0a8e6f6d81b007a42b1c799f5759b68003ca93a922b091450cd66cd08ae60e5c55e306883609f7762a253a07215f40aa03c5c0652348acab9ede4d09848 - languageName: node - linkType: hard - -"@lerna/diff@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/diff@npm:5.5.4" - dependencies: - "@lerna/child-process": 5.5.4 - "@lerna/command": 5.5.4 - "@lerna/validation-error": 5.5.4 - npmlog: ^6.0.2 - checksum: 5a171e653c3074bc1c1d4d200dd2a82cf36e92ad7f9ab03f9d38cd9cd33b44ea0a568c3804d5d15323931ee353f51870ba3f000a917c041df77a8412bcffdb2f - languageName: node - linkType: hard - -"@lerna/exec@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/exec@npm:5.5.4" - dependencies: - "@lerna/child-process": 5.5.4 - "@lerna/command": 5.5.4 - "@lerna/filter-options": 5.5.4 - "@lerna/profiler": 5.5.4 - "@lerna/run-topologically": 5.5.4 - "@lerna/validation-error": 5.5.4 - p-map: ^4.0.0 - checksum: 90ba92303def5a1d39e15c3275282e6782005d52c7c880d2d1305f87c6f4a641cff67e2d1ef86dea46c8d18a1a03cb4b21ead7c60306cde5e9fb1d00396086fa - languageName: node - linkType: hard - -"@lerna/filter-options@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/filter-options@npm:5.5.4" - dependencies: - "@lerna/collect-updates": 5.5.4 - "@lerna/filter-packages": 5.5.4 - dedent: ^0.7.0 - npmlog: ^6.0.2 - checksum: a3fc09f042a66373231237fd9521b2d6e35ea56cb2cdf428de80fe7817cf27130718db2ddf68cfd7d7269c34f1cf266208c8e5c8b8dc2fa469fe8c35cbea8ee4 - languageName: node - linkType: hard - -"@lerna/filter-packages@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/filter-packages@npm:5.5.4" - dependencies: - "@lerna/validation-error": 5.5.4 - multimatch: ^5.0.0 - npmlog: ^6.0.2 - checksum: 889c26a659228c041f70ce27c81feaa360e8659299851208c931b726983449a885e943ca50db7c975b670adb07424c83bba0d0f4dc71ea99f9f6143b88a05315 - languageName: node - linkType: hard - -"@lerna/get-npm-exec-opts@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/get-npm-exec-opts@npm:5.5.4" - dependencies: - npmlog: ^6.0.2 - checksum: b4c0e88a53a32eb538b0730b316d7df51281d11d05f2ed928b6ef553c25611af1346921856999b6a8d69ff64742f51dde3e32156dba37ff950206bdebf51048f - languageName: node - linkType: hard - -"@lerna/get-packed@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/get-packed@npm:5.5.4" - dependencies: - fs-extra: ^9.1.0 - ssri: ^9.0.1 - tar: ^6.1.0 - checksum: e53c57740651086c0d5c3eb8aa5a9cd777e7aa9b4ec8bb3d997f1d9df783f67597c6418d73c5d680926998f45fb3ff3735fb72e232098bfb6e788c5fee416a5f - languageName: node - linkType: hard - -"@lerna/github-client@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/github-client@npm:5.5.4" - dependencies: - "@lerna/child-process": 5.5.4 - "@octokit/plugin-enterprise-rest": ^6.0.1 - "@octokit/rest": ^19.0.3 - git-url-parse: ^13.1.0 - npmlog: ^6.0.2 - checksum: 6f531f1c133c2643fa7c716c0d986bf59d18eed13099195fcf4c7fe683b49dc7fad1d410985d3f5273ec0f607763ef800b4d0d841557adc4a3fe4fe05a78dca2 - languageName: node - linkType: hard - -"@lerna/gitlab-client@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/gitlab-client@npm:5.5.4" - dependencies: - node-fetch: ^2.6.1 - npmlog: ^6.0.2 - checksum: bcd867c6e66f5eaa790c2c40a85f88b7fc37dc2504b17d096102630049bcb3dbf6d0ec33e8369fb6204e5537a263bbaeefacae42ab8570e104a5f5a17fa59685 - languageName: node - linkType: hard - -"@lerna/global-options@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/global-options@npm:5.5.4" - checksum: 5a501be802d3bc02f8525a8ee32bab7833e387323b7b52e86c8ed273ed197ca81aa12651a462a98eef11cabd61dca1e5e1a2390023cb056dae30da246ca94b72 - languageName: node - linkType: hard - -"@lerna/has-npm-version@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/has-npm-version@npm:5.5.4" - dependencies: - "@lerna/child-process": 5.5.4 - semver: ^7.3.4 - checksum: e28690f9efc7034da6f0e49b84816a184cea95376088c526621395add30a4e3475f5b66fc4945e126412e72aaa67cf557af236a65d5905084485f0acaf129140 - languageName: node - linkType: hard - -"@lerna/import@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/import@npm:5.5.4" - dependencies: - "@lerna/child-process": 5.5.4 - "@lerna/command": 5.5.4 - "@lerna/prompt": 5.5.4 - "@lerna/pulse-till-done": 5.5.4 - "@lerna/validation-error": 5.5.4 - dedent: ^0.7.0 - fs-extra: ^9.1.0 - p-map-series: ^2.1.0 - checksum: e04b2e85fef25c1ced9f98e607cf6ad25f3e98fe8fe80ae359614c34de4679c45b082a8cae164c6fbe6af86d9ce72128640a225bd603d3faa89d285406adf1f9 - languageName: node - linkType: hard - -"@lerna/info@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/info@npm:5.5.4" - dependencies: - "@lerna/command": 5.5.4 - "@lerna/output": 5.5.4 - envinfo: ^7.7.4 - checksum: 2bfb409a6b60bf2e7f755fe8443adbd3a414a50ea99119294c949770fa800ab6ebbc0a05c6646f2aed1d503ec1b7bfbf06688cfa7ae090d1132b4f041456ac6e - languageName: node - linkType: hard - -"@lerna/init@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/init@npm:5.5.4" - dependencies: - "@lerna/child-process": 5.5.4 - "@lerna/command": 5.5.4 - "@lerna/project": 5.5.4 - fs-extra: ^9.1.0 - p-map: ^4.0.0 - write-json-file: ^4.3.0 - checksum: 610bfc08593d54095898e02adc1e49b742eb385cf6168ec0c134e2f04231fa695924f5bfd404730fc11ae72dfb8700f408fb6514d56333e6e4cc46d4547563a1 - languageName: node - linkType: hard - -"@lerna/link@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/link@npm:5.5.4" - dependencies: - "@lerna/command": 5.5.4 - "@lerna/package-graph": 5.5.4 - "@lerna/symlink-dependencies": 5.5.4 - "@lerna/validation-error": 5.5.4 - p-map: ^4.0.0 - slash: ^3.0.0 - checksum: 391cb0e93f324cb1b7e72c7a415f23ab0690912ff72f0e40b5794ad7c32c4ecd3500f75efac73efd08dc83978aa23bf343b527d2b501d156e8f857ee9fe4f1d9 - languageName: node - linkType: hard - -"@lerna/list@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/list@npm:5.5.4" - dependencies: - "@lerna/command": 5.5.4 - "@lerna/filter-options": 5.5.4 - "@lerna/listable": 5.5.4 - "@lerna/output": 5.5.4 - checksum: ccbea2a102b6c9ebfdfb38bd8fac81a329a8c8bcd466e334ebb7626bf094361fff00a5895523744f12ba3ea4ed4e9798f8ddebc8006fb642f758b79014a3b64e - languageName: node - linkType: hard - -"@lerna/listable@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/listable@npm:5.5.4" - dependencies: - "@lerna/query-graph": 5.5.4 - chalk: ^4.1.0 - columnify: ^1.6.0 - checksum: db4e674fc75f320e5888a2ada8c2447d5111584f3c35e744a6d8fb9a026abe557ad6d02430915aeb7b4cb99cd129f391825053fa785d4ce2db2df462291c8ded - languageName: node - linkType: hard - -"@lerna/log-packed@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/log-packed@npm:5.5.4" - dependencies: - byte-size: ^7.0.0 - columnify: ^1.6.0 - has-unicode: ^2.0.1 - npmlog: ^6.0.2 - checksum: 83af3e1b63e658b9fde75feb9508a38492f23743ce028d74bde674ffba01bb7d9b0607f751f42c3b805c6b1a5c2ca70cd4e7ec70997f200875ce5ff4e4798e51 - languageName: node - linkType: hard - -"@lerna/npm-conf@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/npm-conf@npm:5.5.4" - dependencies: - config-chain: ^1.1.12 - pify: ^5.0.0 - checksum: dfbad876fd5bb92d6a34f0f4ec58eeaeaea323cd40be0e7b7cc8235093af7c855be3fd1aa689992aa884e404f5a16d38ea7a4c244f112b43f630d18aef8a162d - languageName: node - linkType: hard - -"@lerna/npm-dist-tag@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/npm-dist-tag@npm:5.5.4" - dependencies: - "@lerna/otplease": 5.5.4 - npm-package-arg: 8.1.1 - npm-registry-fetch: ^13.3.0 - npmlog: ^6.0.2 - checksum: 4fb1fa54c1dd2e6a5c5ab68723a753bc74542f7031e513c5ada7507496f3dae10884a6727486024b1b6f272c81d4e1a1f63dfc012180d3e27b896689dc79f402 - languageName: node - linkType: hard - -"@lerna/npm-install@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/npm-install@npm:5.5.4" - dependencies: - "@lerna/child-process": 5.5.4 - "@lerna/get-npm-exec-opts": 5.5.4 - fs-extra: ^9.1.0 - npm-package-arg: 8.1.1 - npmlog: ^6.0.2 - signal-exit: ^3.0.3 - write-pkg: ^4.0.0 - checksum: 156524225ab1e504e86aa025c464b1557f04e0cc72ff1c288afe69334eea3b394640ee0e48582e9cae357daa186a14066a8ad6be12eb4f497ac1cbe5bc3f1df5 - languageName: node - linkType: hard - -"@lerna/npm-publish@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/npm-publish@npm:5.5.4" - dependencies: - "@lerna/otplease": 5.5.4 - "@lerna/run-lifecycle": 5.5.4 - fs-extra: ^9.1.0 - libnpmpublish: ^6.0.4 - npm-package-arg: 8.1.1 - npmlog: ^6.0.2 - pify: ^5.0.0 - read-package-json: ^5.0.1 - checksum: 6a28621b59bc91a411c78f87d2b6aa9bdfd406cd7b2e05b448c5e94fdcab4643933cbc6b708e013ad158f9b839bb0ac8d305b92a2bd398ebc3e7fed72407af27 - languageName: node - linkType: hard - -"@lerna/npm-run-script@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/npm-run-script@npm:5.5.4" - dependencies: - "@lerna/child-process": 5.5.4 - "@lerna/get-npm-exec-opts": 5.5.4 - npmlog: ^6.0.2 - checksum: 488d32847ac2f15e7d8c5ef3564438029fecc92da5b643f5b642bb0706e041ca93c753b36f0325a4c62e78226379de201a1eb5b65c32287421dee3d0fb0d84c0 - languageName: node - linkType: hard - -"@lerna/otplease@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/otplease@npm:5.5.4" - dependencies: - "@lerna/prompt": 5.5.4 - checksum: 13970bae350dbbc58588a475ee706b1c5ecff556dfbc9858da0d0fc75f5687d608e0bd134260c354fbbc0108d9bdac6b2f2cc3e1c61f5cba9c188210835eae7e - languageName: node - linkType: hard - -"@lerna/output@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/output@npm:5.5.4" - dependencies: - npmlog: ^6.0.2 - checksum: f72633c06f8052c8283d039e10fa7aa4e9c965d6c838bdc736bd51d635a0ba20e173fc2e19173f3537b6d94a5881ac964b10ebaf99704ec5105a303563b64afe - languageName: node - linkType: hard - -"@lerna/pack-directory@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/pack-directory@npm:5.5.4" - dependencies: - "@lerna/get-packed": 5.5.4 - "@lerna/package": 5.5.4 - "@lerna/run-lifecycle": 5.5.4 - "@lerna/temp-write": 5.5.4 - npm-packlist: ^5.1.1 - npmlog: ^6.0.2 - tar: ^6.1.0 - checksum: 1d31a76e463957e8441e53d96b228da92daa1c2c242a9a1d2b4a4e95fca710f3b6f9e01f2f129cb33f0a9bf3d3e63ae579cf8f79f48c774ba604078b90b8775b - languageName: node - linkType: hard - -"@lerna/package-graph@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/package-graph@npm:5.5.4" - dependencies: - "@lerna/prerelease-id-from-version": 5.5.4 - "@lerna/validation-error": 5.5.4 - npm-package-arg: 8.1.1 - npmlog: ^6.0.2 - semver: ^7.3.4 - checksum: 4e48d8993eec4e381f817535e0f45472191889ba596e812941caeaf7fd57b28c7cd24e27ad23085db5ad1df6a82498812521b9698e2fad5d8be23d2d7a376f9d - languageName: node - linkType: hard - -"@lerna/package@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/package@npm:5.5.4" - dependencies: - load-json-file: ^6.2.0 - npm-package-arg: 8.1.1 - write-pkg: ^4.0.0 - checksum: 9e2ef5f6c43f02f8f81ccc33b6a195f5a3b505e0112a8a72c8169211249abeff1ccc1480a6f9dcf7e23068541cefa0eb26541fabf867d452657145aaef88dd6b - languageName: node - linkType: hard - -"@lerna/prerelease-id-from-version@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/prerelease-id-from-version@npm:5.5.4" - dependencies: - semver: ^7.3.4 - checksum: 6213fe4dc060d7c41e153e4e6c1f6214bad88e911659846ea39ccc874801d276c7751b836cf0bf17cb63e45943f1c67396717f6843fe58c6a31806a49c26cbd7 - languageName: node - linkType: hard - -"@lerna/profiler@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/profiler@npm:5.5.4" - dependencies: - fs-extra: ^9.1.0 - npmlog: ^6.0.2 - upath: ^2.0.1 - checksum: 1c3eb01eccf7d478ee3197886b60d43f743058c9f2591bf40d0b0737d263af3169a9d55d42fcc7b02c41d60cba1ac4bee1f146318c4c2e85a9892b0190b2d3ae - languageName: node - linkType: hard - -"@lerna/project@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/project@npm:5.5.4" - dependencies: - "@lerna/package": 5.5.4 - "@lerna/validation-error": 5.5.4 - cosmiconfig: ^7.0.0 - dedent: ^0.7.0 - dot-prop: ^6.0.1 - glob-parent: ^5.1.1 - globby: ^11.0.2 - js-yaml: ^4.1.0 - load-json-file: ^6.2.0 - npmlog: ^6.0.2 - p-map: ^4.0.0 - resolve-from: ^5.0.0 - write-json-file: ^4.3.0 - checksum: a97b76a6fc655e7d3177f9bfeff4da5e2b353322dfbeb70ae619fdd2b69f0ab6b2d2cb772388f396f8b21fbe0fade882303fbed9fb5c476a26f9b3b7aaa722dc - languageName: node - linkType: hard - -"@lerna/prompt@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/prompt@npm:5.5.4" +"@lerna/create@npm:7.4.1": + version: 7.4.1 + resolution: "@lerna/create@npm:7.4.1" dependencies: + "@lerna/child-process": 7.4.1 + "@npmcli/run-script": 6.0.2 + "@nx/devkit": ">=16.5.1 < 17" + "@octokit/plugin-enterprise-rest": 6.0.1 + "@octokit/rest": 19.0.11 + byte-size: 8.1.1 + chalk: 4.1.0 + clone-deep: 4.0.1 + cmd-shim: 6.0.1 + columnify: 1.6.0 + conventional-changelog-core: 5.0.1 + conventional-recommended-bump: 7.0.1 + cosmiconfig: ^8.2.0 + dedent: 0.7.0 + execa: 5.0.0 + fs-extra: ^11.1.1 + get-stream: 6.0.0 + git-url-parse: 13.1.0 + glob-parent: 5.1.2 + globby: 11.1.0 + graceful-fs: 4.2.11 + has-unicode: 2.0.1 + ini: ^1.3.8 + init-package-json: 5.0.0 inquirer: ^8.2.4 - npmlog: ^6.0.2 - checksum: 652293aac0a159bc4eea11c85014e8368447f36b18810b7c92f3c33614890e74528d77ddceccd7ea038b3aa7f3976428329b34ca6f7cad8b424502bf64240b40 - languageName: node - linkType: hard - -"@lerna/publish@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/publish@npm:5.5.4" - dependencies: - "@lerna/check-working-tree": 5.5.4 - "@lerna/child-process": 5.5.4 - "@lerna/collect-updates": 5.5.4 - "@lerna/command": 5.5.4 - "@lerna/describe-ref": 5.5.4 - "@lerna/log-packed": 5.5.4 - "@lerna/npm-conf": 5.5.4 - "@lerna/npm-dist-tag": 5.5.4 - "@lerna/npm-publish": 5.5.4 - "@lerna/otplease": 5.5.4 - "@lerna/output": 5.5.4 - "@lerna/pack-directory": 5.5.4 - "@lerna/prerelease-id-from-version": 5.5.4 - "@lerna/prompt": 5.5.4 - "@lerna/pulse-till-done": 5.5.4 - "@lerna/run-lifecycle": 5.5.4 - "@lerna/run-topologically": 5.5.4 - "@lerna/validation-error": 5.5.4 - "@lerna/version": 5.5.4 - fs-extra: ^9.1.0 - libnpmaccess: ^6.0.3 + is-ci: 3.0.1 + is-stream: 2.0.0 + js-yaml: 4.1.0 + libnpmpublish: 7.3.0 + load-json-file: 6.2.0 + lodash: ^4.17.21 + make-dir: 4.0.0 + minimatch: 3.0.5 + multimatch: 5.0.0 + node-fetch: 2.6.7 npm-package-arg: 8.1.1 - npm-registry-fetch: ^13.3.0 + npm-packlist: 5.1.1 + npm-registry-fetch: ^14.0.5 npmlog: ^6.0.2 - p-map: ^4.0.0 - p-pipe: ^3.1.0 - pacote: ^13.6.1 - semver: ^7.3.4 - checksum: 466de9cade594c1f9bcb28f6e68dd51b7180a2eda864b0a55b46ddca59250ed7b91c996bd2f8a10ce6024e8f9b914561832b07cf149c8e7c66d596a3159591cc - languageName: node - linkType: hard - -"@lerna/pulse-till-done@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/pulse-till-done@npm:5.5.4" - dependencies: - npmlog: ^6.0.2 - checksum: a296b1617590188ad51da84020afc09db906a98c2d9b993bb93db493a8d9297dfdecdedc31e7f52c996e3b9fec9891c4d8dea7f5a7da912b8e711aa320ab6901 - languageName: node - linkType: hard - -"@lerna/query-graph@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/query-graph@npm:5.5.4" - dependencies: - "@lerna/package-graph": 5.5.4 - checksum: b360f980ff5ab5706a61b11b5d3ea256a729142b52af5c41a926e481b9722c8a04ca9d0c9a4a474a6258c49365ae8fdc3188be37a97acd48cec2174a09a9bb52 - languageName: node - linkType: hard - -"@lerna/resolve-symlink@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/resolve-symlink@npm:5.5.4" - dependencies: - fs-extra: ^9.1.0 - npmlog: ^6.0.2 - read-cmd-shim: ^3.0.0 - checksum: b3ddc7c92404385d6ba8a67cd63594192f84655af19cbe5fcc781f4d0d09348c480bf6d8eb86b689a02dac0e729fbf97355f3a6645312098992d2a47d1db57a0 - languageName: node - linkType: hard - -"@lerna/rimraf-dir@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/rimraf-dir@npm:5.5.4" - dependencies: - "@lerna/child-process": 5.5.4 - npmlog: ^6.0.2 - path-exists: ^4.0.0 - rimraf: ^3.0.2 - checksum: fd7255b7fcd895db588eb01b2c4387d9f43b0b618b1dfba426250545e2c3e3586bdaf8274ea4631c5f574cb973e6daf2c3fda1aee69dfa75d4ee5b681d8689dc - languageName: node - linkType: hard - -"@lerna/run-lifecycle@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/run-lifecycle@npm:5.5.4" - dependencies: - "@lerna/npm-conf": 5.5.4 - "@npmcli/run-script": ^4.1.7 - npmlog: ^6.0.2 - p-queue: ^6.6.2 - checksum: 3bbf90da32e83f3a909b190a174c215aa14ebc90c2eba62de79216d6c3c08411abba03d3dee1f2146c5552a36b9507ec0550ce38769265a5ede5f1f8c0b1fd75 - languageName: node - linkType: hard - -"@lerna/run-topologically@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/run-topologically@npm:5.5.4" - dependencies: - "@lerna/query-graph": 5.5.4 - p-queue: ^6.6.2 - checksum: 2fc3f2bcc6180e6b41e8cbc5bda35180c1fc6f69e6cf17306bc532c8d59667104dc3f65e6f05669b9cd222e06788ea4d110503bd26f75204657edb01d7389ae0 - languageName: node - linkType: hard - -"@lerna/run@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/run@npm:5.5.4" - dependencies: - "@lerna/command": 5.5.4 - "@lerna/filter-options": 5.5.4 - "@lerna/npm-run-script": 5.5.4 - "@lerna/output": 5.5.4 - "@lerna/profiler": 5.5.4 - "@lerna/run-topologically": 5.5.4 - "@lerna/timer": 5.5.4 - "@lerna/validation-error": 5.5.4 - fs-extra: ^9.1.0 - p-map: ^4.0.0 - checksum: aab32307bf40ff5c6bd061deaa1911068f1f02125b599148e1b7b8344ce26209ba7807f86a713531710fe7ffb46186980ffaf801b3f3151afb8e4952b5ab6ef6 - languageName: node - linkType: hard - -"@lerna/symlink-binary@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/symlink-binary@npm:5.5.4" - dependencies: - "@lerna/create-symlink": 5.5.4 - "@lerna/package": 5.5.4 - fs-extra: ^9.1.0 - p-map: ^4.0.0 - checksum: a4bff1050a379f237fbcfe7145ea1b3ad0ccce3daff44be19dd3ca96f542d78cec24be7ac14ef97163fb4024c679ee1d748c8035faf4cedc3a0270fc965b9f4d - languageName: node - linkType: hard - -"@lerna/symlink-dependencies@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/symlink-dependencies@npm:5.5.4" - dependencies: - "@lerna/create-symlink": 5.5.4 - "@lerna/resolve-symlink": 5.5.4 - "@lerna/symlink-binary": 5.5.4 - fs-extra: ^9.1.0 - p-map: ^4.0.0 - p-map-series: ^2.1.0 - checksum: d7675966af7cb83a8e0a963e4b08049be4cdf4381196d4fbc975f571a5b3f5f0bf5002b5c15a5256ae861bacf86856469fa2bc90f64270998f4ac529bbdb08ed - languageName: node - linkType: hard - -"@lerna/temp-write@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/temp-write@npm:5.5.4" - dependencies: - graceful-fs: ^4.1.15 - is-stream: ^2.0.0 - make-dir: ^3.0.0 - temp-dir: ^1.0.0 - uuid: ^8.3.2 - checksum: f9d61e997dd10d4445da1f85582c4649b8ee1bcf3e9cd3ce52013cc88d3f39b7b26445dfe7a222eb5fde2687d759281a5222994fe1dcf1fa5f3d68dec22a51a6 - languageName: node - linkType: hard - -"@lerna/timer@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/timer@npm:5.5.4" - checksum: b15c10881a5e2e8e6c42643bdb1d1e0c73930545f522f161fb24c007bf373b8d7d59694cd24d1ea17bc631f72bd40f45095a538bd86b03fa7161febec4ec94c5 - languageName: node - linkType: hard - -"@lerna/validation-error@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/validation-error@npm:5.5.4" - dependencies: - npmlog: ^6.0.2 - checksum: 86cc66dd8f3d35ff333ad6e5009f176cb21c3e073e4591dc714f18d7f8d44cd3838ae365d5e9d927d7d8a08248296b76048f7d5948052bddecc9920ad8f5a013 - languageName: node - linkType: hard - -"@lerna/version@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/version@npm:5.5.4" - dependencies: - "@lerna/check-working-tree": 5.5.4 - "@lerna/child-process": 5.5.4 - "@lerna/collect-updates": 5.5.4 - "@lerna/command": 5.5.4 - "@lerna/conventional-commits": 5.5.4 - "@lerna/github-client": 5.5.4 - "@lerna/gitlab-client": 5.5.4 - "@lerna/output": 5.5.4 - "@lerna/prerelease-id-from-version": 5.5.4 - "@lerna/prompt": 5.5.4 - "@lerna/run-lifecycle": 5.5.4 - "@lerna/run-topologically": 5.5.4 - "@lerna/temp-write": 5.5.4 - "@lerna/validation-error": 5.5.4 - chalk: ^4.1.0 - dedent: ^0.7.0 - load-json-file: ^6.2.0 - minimatch: ^3.0.4 - npmlog: ^6.0.2 - p-map: ^4.0.0 - p-pipe: ^3.1.0 + nx: ">=16.5.1 < 17" + p-map: 4.0.0 + p-map-series: 2.1.0 + p-queue: 6.6.2 p-reduce: ^2.1.0 - p-waterfall: ^2.1.1 + pacote: ^15.2.0 + pify: 5.0.0 + read-cmd-shim: 4.0.0 + read-package-json: 6.0.4 + resolve-from: 5.0.0 + rimraf: ^4.4.1 semver: ^7.3.4 + signal-exit: 3.0.7 slash: ^3.0.0 - write-json-file: ^4.3.0 - checksum: 785d1cfb837cd6c2559a4f777ee621fd760019d591b6e878b2845d257595bdad4eb3a11710beac91e1a2244b95f7ca708b576ecf59a8747cc85de074b7f0e086 - languageName: node - linkType: hard - -"@lerna/write-log-file@npm:5.5.4": - version: 5.5.4 - resolution: "@lerna/write-log-file@npm:5.5.4" - dependencies: - npmlog: ^6.0.2 - write-file-atomic: ^4.0.1 - checksum: c25c235f5399e0d510a4b6d7b3a675d0a8dabd8b84dd663fbf713a4174e234e21bc1b5d35c50f442cc4e57000482955bbd351fbd9181cd56fe4f5dd730ada001 + ssri: ^9.0.1 + strong-log-transformer: 2.1.0 + tar: 6.1.11 + temp-dir: 1.0.0 + upath: 2.0.1 + uuid: ^9.0.0 + validate-npm-package-license: ^3.0.4 + validate-npm-package-name: 5.0.0 + write-file-atomic: 5.0.1 + write-pkg: 4.0.0 + yargs: 16.2.0 + yargs-parser: 20.2.4 + checksum: c0762cf8bb127a6c59c714dc0e0382f1d8f65f3f68ac968bf85798f2c32e0483d589a1a6ccfc03f98d036e8b3841e842159bcb81c5b4212464c2a328745e325e languageName: node linkType: hard @@ -5058,50 +4331,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/arborist@npm:5.3.0": - version: 5.3.0 - resolution: "@npmcli/arborist@npm:5.3.0" - dependencies: - "@isaacs/string-locale-compare": ^1.1.0 - "@npmcli/installed-package-contents": ^1.0.7 - "@npmcli/map-workspaces": ^2.0.3 - "@npmcli/metavuln-calculator": ^3.0.1 - "@npmcli/move-file": ^2.0.0 - "@npmcli/name-from-folder": ^1.0.1 - "@npmcli/node-gyp": ^2.0.0 - "@npmcli/package-json": ^2.0.0 - "@npmcli/run-script": ^4.1.3 - bin-links: ^3.0.0 - cacache: ^16.0.6 - common-ancestor-path: ^1.0.1 - json-parse-even-better-errors: ^2.3.1 - json-stringify-nice: ^1.1.4 - mkdirp: ^1.0.4 - mkdirp-infer-owner: ^2.0.0 - nopt: ^5.0.0 - npm-install-checks: ^5.0.0 - npm-package-arg: ^9.0.0 - npm-pick-manifest: ^7.0.0 - npm-registry-fetch: ^13.0.0 - npmlog: ^6.0.2 - pacote: ^13.6.1 - parse-conflict-json: ^2.0.1 - proc-log: ^2.0.0 - promise-all-reject-late: ^1.0.0 - promise-call-limit: ^1.0.1 - read-package-json-fast: ^2.0.2 - readdir-scoped-modules: ^1.1.0 - rimraf: ^3.0.2 - semver: ^7.3.7 - ssri: ^9.0.0 - treeverse: ^2.0.0 - walk-up-path: ^1.0.0 - bin: - arborist: bin/index.js - checksum: 7f99f451ba625dd3532e7a69b27cc399cab1e7ef2a069bbc04cf22ef9d16a0076f8f5fb92c4cd146c256cd8a41963b2e417684f063a108e96939c440bad0e95e - languageName: node - linkType: hard - "@npmcli/fs@npm:^1.0.0": version: 1.0.0 resolution: "@npmcli/fs@npm:1.0.0" @@ -5122,56 +4351,40 @@ __metadata: languageName: node linkType: hard -"@npmcli/git@npm:^3.0.0": - version: 3.0.1 - resolution: "@npmcli/git@npm:3.0.1" +"@npmcli/fs@npm:^3.1.0": + version: 3.1.0 + resolution: "@npmcli/fs@npm:3.1.0" dependencies: - "@npmcli/promise-spawn": ^3.0.0 + semver: ^7.3.5 + checksum: a50a6818de5fc557d0b0e6f50ec780a7a02ab8ad07e5ac8b16bf519e0ad60a144ac64f97d05c443c3367235d337182e1d012bbac0eb8dbae8dc7b40b193efd0e + languageName: node + linkType: hard + +"@npmcli/git@npm:^4.0.0": + version: 4.1.0 + resolution: "@npmcli/git@npm:4.1.0" + dependencies: + "@npmcli/promise-spawn": ^6.0.0 lru-cache: ^7.4.4 - mkdirp: ^1.0.4 - npm-pick-manifest: ^7.0.0 - proc-log: ^2.0.0 + npm-pick-manifest: ^8.0.0 + proc-log: ^3.0.0 promise-inflight: ^1.0.1 promise-retry: ^2.0.1 semver: ^7.3.5 - which: ^2.0.2 - checksum: 0e289d11e2d6034652993f2d05f68396d8377603a1c1f983b2d0893e7591a22bcf3896a43c7dfbcc43f03c308a110f0b9ec37e0191e48b0bd1d236e0f57a3ec6 + which: ^3.0.0 + checksum: 37efb926593f294eb263297cdfffec9141234f977b89a7a6b95ff7a72576c1d7f053f4961bc4b5e79dea6476fe08e0f3c1ed9e4aeb84169e357ff757a6a70073 languageName: node linkType: hard -"@npmcli/installed-package-contents@npm:^1.0.7": - version: 1.0.7 - resolution: "@npmcli/installed-package-contents@npm:1.0.7" +"@npmcli/installed-package-contents@npm:^2.0.1": + version: 2.0.2 + resolution: "@npmcli/installed-package-contents@npm:2.0.2" dependencies: - npm-bundled: ^1.1.1 - npm-normalize-package-bin: ^1.0.1 + npm-bundled: ^3.0.0 + npm-normalize-package-bin: ^3.0.0 bin: - installed-package-contents: index.js - checksum: a4a29b99d439827ce2e7817c1f61b56be160e640696e31dc513a2c8a37c792f75cdb6258ec15a1e22904f20df0a8a3019dd3766de5e6619f259834cf64233538 - languageName: node - linkType: hard - -"@npmcli/map-workspaces@npm:^2.0.3": - version: 2.0.3 - resolution: "@npmcli/map-workspaces@npm:2.0.3" - dependencies: - "@npmcli/name-from-folder": ^1.0.1 - glob: ^8.0.1 - minimatch: ^5.0.1 - read-package-json-fast: ^2.0.3 - checksum: c9878a22168d3f2d8df9e339ed0799628db3ea8502bd623b5bbe7b0dfcac065b3310e4093df94667a4a28ef2c54c02ce6956467a8aaa2e150305f2fe1cd64f9d - languageName: node - linkType: hard - -"@npmcli/metavuln-calculator@npm:^3.0.1": - version: 3.1.1 - resolution: "@npmcli/metavuln-calculator@npm:3.1.1" - dependencies: - cacache: ^16.0.0 - json-parse-even-better-errors: ^2.3.1 - pacote: ^13.0.3 - semver: ^7.3.5 - checksum: dc9846fdb82a1f4274ff8943f81452c75615bd9bca523c862956ea2c32e18c5a4be5572e169104d3a0eb262b7ede72c8dbbc202a4ab3b3f4946fa55f226dcc64 + installed-package-contents: lib/index.js + checksum: 60789d5ed209ee5df479232f62d9d38ecec36e95701cae88320b828b8651351b32d7b47d16d4c36cc7ce5000db4bf1f3e6981bed6381bdc5687ff4bc0795682d languageName: node linkType: hard @@ -5195,68 +4408,140 @@ __metadata: languageName: node linkType: hard -"@npmcli/name-from-folder@npm:^1.0.1": - version: 1.0.1 - resolution: "@npmcli/name-from-folder@npm:1.0.1" - checksum: 67339f4096e32b712d2df0250cc95c087569f09e657d7f81a1760fa2cc5123e29c3c3e1524388832310ba2d96ec4679985b643b44627f6a51f4a00c3b0075de9 - languageName: node - linkType: hard - -"@npmcli/node-gyp@npm:^2.0.0": - version: 2.0.0 - resolution: "@npmcli/node-gyp@npm:2.0.0" - checksum: b6bbf0015000f9b64d31aefdc30f244b0348c57adb64017667e0304e96c38644d83da46a4581252652f5d606268df49118f9c9993b41d8020f62b7b15dd2c8d8 - languageName: node - linkType: hard - -"@npmcli/package-json@npm:^2.0.0": - version: 2.0.0 - resolution: "@npmcli/package-json@npm:2.0.0" - dependencies: - json-parse-even-better-errors: ^2.3.1 - checksum: 7a598e42d2778654ec87438ebfafbcbafbe5a5f5e89ed2ca1db6ca3f94ef14655e304aa41f77632a2a3f5c66b6bd5960bd9370e0ceb4902ea09346720364f9e4 - languageName: node - linkType: hard - -"@npmcli/promise-spawn@npm:^3.0.0": +"@npmcli/node-gyp@npm:^3.0.0": version: 3.0.0 - resolution: "@npmcli/promise-spawn@npm:3.0.0" - dependencies: - infer-owner: ^1.0.4 - checksum: 3454465a2731cea5875ba51f80873e2205e5bd878c31517286b0ede4ea931c7bf3de895382287e906d03710fff6f9e44186bd0eee068ce578901c5d3b58e7692 + resolution: "@npmcli/node-gyp@npm:3.0.0" + checksum: fe3802b813eecb4ade7ad77c9396cb56721664275faab027e3bd8a5e15adfbbe39e2ecc19f7885feb3cfa009b96632741cc81caf7850ba74440c6a2eee7b4ffc languageName: node linkType: hard -"@npmcli/run-script@npm:^4.1.0, @npmcli/run-script@npm:^4.1.3, @npmcli/run-script@npm:^4.1.7": - version: 4.2.1 - resolution: "@npmcli/run-script@npm:4.2.1" +"@npmcli/promise-spawn@npm:^6.0.0, @npmcli/promise-spawn@npm:^6.0.1": + version: 6.0.2 + resolution: "@npmcli/promise-spawn@npm:6.0.2" dependencies: - "@npmcli/node-gyp": ^2.0.0 - "@npmcli/promise-spawn": ^3.0.0 + which: ^3.0.0 + checksum: aa725780c13e1f97ab32ed7bcb5a207a3fb988e1d7ecdc3d22a549a22c8034740366b351c4dde4b011bcffcd8c4a7be6083d9cf7bc7e897b88837150de018528 + languageName: node + linkType: hard + +"@npmcli/run-script@npm:6.0.2, @npmcli/run-script@npm:^6.0.0": + version: 6.0.2 + resolution: "@npmcli/run-script@npm:6.0.2" + dependencies: + "@npmcli/node-gyp": ^3.0.0 + "@npmcli/promise-spawn": ^6.0.0 node-gyp: ^9.0.0 - read-package-json-fast: ^2.0.3 - which: ^2.0.2 - checksum: 7b8d6676353f157e68b26baf848e01e5d887bcf90ce81a52f23fc9a5d93e6ffb60057532d664cfd7aeeb76d464d0c8b0d314ee6cccb56943acb3b6c570b756c8 + read-package-json-fast: ^3.0.0 + which: ^3.0.0 + checksum: 7a671d7dbeae376496e1c6242f02384928617dc66cd22881b2387272205c3668f8490ec2da4ad63e1abf979efdd2bdf4ea0926601d78578e07d83cfb233b3a1a languageName: node linkType: hard -"@nrwl/cli@npm:14.8.2": - version: 14.8.2 - resolution: "@nrwl/cli@npm:14.8.2" +"@nrwl/devkit@npm:16.10.0": + version: 16.10.0 + resolution: "@nrwl/devkit@npm:16.10.0" dependencies: - nx: 14.8.2 - checksum: 18d698397cd0536109b1a6dbe50e9ec13063dde2793b49ab25d3db3f55ec74931ad20ae32375c5d2a1554d9c91f5b1152e42d6738aaca3ebecca4735bd4916c8 + "@nx/devkit": 16.10.0 + checksum: 92c40138f7d107da82d14adca1cedb16ff45583f486cf624d047b2928521f92da6f69a5bdeae0bd98a37dfa553883843f36088ee6ace8d76a5170a5730b89a40 languageName: node linkType: hard -"@nrwl/tao@npm:14.8.2": - version: 14.8.2 - resolution: "@nrwl/tao@npm:14.8.2" +"@nrwl/tao@npm:16.10.0": + version: 16.10.0 + resolution: "@nrwl/tao@npm:16.10.0" dependencies: - nx: 14.8.2 + nx: 16.10.0 + tslib: ^2.3.0 bin: tao: index.js - checksum: 78067a5c61b88c7cc43b0313dd1a96cc40149b84f349f2c634dd8ee5514b9d71deca28267a03fa081c8c5877d406e3774046c4927f0111c9f5c5571fd617e254 + checksum: a973a9fbed8fea33bfcb1b39b4bb29371ea00d116bbe7e39f2e7c8a9448b86e7c499d0aef79f262d9a993d103b4451d6749889e307212421b10838d49454a35c + languageName: node + linkType: hard + +"@nx/devkit@npm:16.10.0, @nx/devkit@npm:>=16.5.1 < 17": + version: 16.10.0 + resolution: "@nx/devkit@npm:16.10.0" + dependencies: + "@nrwl/devkit": 16.10.0 + ejs: ^3.1.7 + enquirer: ~2.3.6 + ignore: ^5.0.4 + semver: 7.5.3 + tmp: ~0.2.1 + tslib: ^2.3.0 + peerDependencies: + nx: ">= 15 <= 17" + checksum: f79f22be16d216aabc12df06f4f6d93026082c86114a99a66915f2993b4052ee8c66fd8eccad916e487a3f012890b89c4dd6a2ca8f3f95150ac824fab187a55a + languageName: node + linkType: hard + +"@nx/nx-darwin-arm64@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-darwin-arm64@npm:16.10.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@nx/nx-darwin-x64@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-darwin-x64@npm:16.10.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@nx/nx-freebsd-x64@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-freebsd-x64@npm:16.10.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@nx/nx-linux-arm-gnueabihf@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-linux-arm-gnueabihf@npm:16.10.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@nx/nx-linux-arm64-gnu@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-linux-arm64-gnu@npm:16.10.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@nx/nx-linux-arm64-musl@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-linux-arm64-musl@npm:16.10.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@nx/nx-linux-x64-gnu@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-linux-x64-gnu@npm:16.10.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@nx/nx-linux-x64-musl@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-linux-x64-musl@npm:16.10.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@nx/nx-win32-arm64-msvc@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-win32-arm64-msvc@npm:16.10.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@nx/nx-win32-x64-msvc@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-win32-x64-msvc@npm:16.10.0" + conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -5269,18 +4554,18 @@ __metadata: languageName: node linkType: hard -"@octokit/core@npm:^4.0.0": - version: 4.0.4 - resolution: "@octokit/core@npm:4.0.4" +"@octokit/core@npm:^4.2.1": + version: 4.2.4 + resolution: "@octokit/core@npm:4.2.4" dependencies: "@octokit/auth-token": ^3.0.0 "@octokit/graphql": ^5.0.0 "@octokit/request": ^6.0.0 "@octokit/request-error": ^3.0.0 - "@octokit/types": ^6.0.3 + "@octokit/types": ^9.0.0 before-after-hook: ^2.2.0 universal-user-agent: ^6.0.0 - checksum: c9ae1e5706ab568a725cc5dba314049fbd37d77f1595dd2c19733abddfd72f4e1d46d6980e212d845dde4625ce5f170af951ac0eb0d7bc09e56a159b88cbe5dd + checksum: ac8ab47440a31b0228a034aacac6994b64d6b073ad5b688b4c5157fc5ee0d1af1c926e6087bf17fd7244ee9c5998839da89065a90819bde4a97cb77d4edf58a6 languageName: node linkType: hard @@ -5313,21 +4598,29 @@ __metadata: languageName: node linkType: hard -"@octokit/plugin-enterprise-rest@npm:^6.0.1": +"@octokit/openapi-types@npm:^18.0.0": + version: 18.1.1 + resolution: "@octokit/openapi-types@npm:18.1.1" + checksum: 94f42977fd2fcb9983c781fd199bc11218885a1226d492680bfb1268524a1b2af48a768eef90c63b80a2874437de641d59b3b7f640a5afa93e7c21fe1a79069a + languageName: node + linkType: hard + +"@octokit/plugin-enterprise-rest@npm:6.0.1": version: 6.0.1 resolution: "@octokit/plugin-enterprise-rest@npm:6.0.1" checksum: 1c9720002f31daf62f4f48e73557dcdd7fcde6e0f6d43256e3f2ec827b5548417297186c361fb1af497fdcc93075a7b681e6ff06e2f20e4a8a3e74cc09d1f7e3 languageName: node linkType: hard -"@octokit/plugin-paginate-rest@npm:^3.0.0": - version: 3.0.0 - resolution: "@octokit/plugin-paginate-rest@npm:3.0.0" +"@octokit/plugin-paginate-rest@npm:^6.1.2": + version: 6.1.2 + resolution: "@octokit/plugin-paginate-rest@npm:6.1.2" dependencies: - "@octokit/types": ^6.39.0 + "@octokit/tsconfig": ^1.0.2 + "@octokit/types": ^9.2.3 peerDependencies: "@octokit/core": ">=4" - checksum: 1d2c900254f3dcd43f7ba69dfd12ff63f93a0d39a1bf542b1d0f006e95da4924ae0a26044c864ad7fb0309047f44becaf76293aae334d14c946910d65edd2523 + checksum: a7b3e686c7cbd27ec07871cde6e0b1dc96337afbcef426bbe3067152a17b535abd480db1861ca28c88d93db5f7bfdbcadd0919ead19818c28a69d0e194038065 languageName: node linkType: hard @@ -5340,15 +4633,14 @@ __metadata: languageName: node linkType: hard -"@octokit/plugin-rest-endpoint-methods@npm:^6.0.0": - version: 6.1.2 - resolution: "@octokit/plugin-rest-endpoint-methods@npm:6.1.2" +"@octokit/plugin-rest-endpoint-methods@npm:^7.1.2": + version: 7.2.3 + resolution: "@octokit/plugin-rest-endpoint-methods@npm:7.2.3" dependencies: - "@octokit/types": ^6.40.0 - deprecation: ^2.3.1 + "@octokit/types": ^10.0.0 peerDependencies: "@octokit/core": ">=3" - checksum: 88ba028da00f73cf8a0471e9ffe0da2ba61c15b91fbaf252e33863bd4115db7dcd3bd426f22a01c7492affe89e37117fc3f0d15569cd5cf5a4b978f1bc22738b + checksum: 21dfb98514dbe900c29cddb13b335bbce43d613800c6b17eba3c1fd31d17e69c1960f3067f7bf864bb38fdd5043391f4a23edee42729d8c7fbabd00569a80336 languageName: node linkType: hard @@ -5377,19 +4669,35 @@ __metadata: languageName: node linkType: hard -"@octokit/rest@npm:^19.0.3": - version: 19.0.3 - resolution: "@octokit/rest@npm:19.0.3" +"@octokit/rest@npm:19.0.11": + version: 19.0.11 + resolution: "@octokit/rest@npm:19.0.11" dependencies: - "@octokit/core": ^4.0.0 - "@octokit/plugin-paginate-rest": ^3.0.0 + "@octokit/core": ^4.2.1 + "@octokit/plugin-paginate-rest": ^6.1.2 "@octokit/plugin-request-log": ^1.0.4 - "@octokit/plugin-rest-endpoint-methods": ^6.0.0 - checksum: 9ee96976c4c22dab11b3dacd541e694f3ad9bb1d44243985dc90ce6e8a42c3e3176a206e8d3a883b63b517fc15af8c8c88d8d0ecd9bac2b86a635a9667fc6ff4 + "@octokit/plugin-rest-endpoint-methods": ^7.1.2 + checksum: 147518ad51d214ead88adc717b5fdc4f33317949d58c124f4069bdf07d2e6b49fa66861036b9e233aed71fcb88ff367a6da0357653484e466175ab4fb7183b3b languageName: node linkType: hard -"@octokit/types@npm:^6.0.3, @octokit/types@npm:^6.16.1, @octokit/types@npm:^6.39.0, @octokit/types@npm:^6.40.0": +"@octokit/tsconfig@npm:^1.0.2": + version: 1.0.2 + resolution: "@octokit/tsconfig@npm:1.0.2" + checksum: 74d56f3e9f326a8dd63700e9a51a7c75487180629c7a68bbafee97c612fbf57af8347369bfa6610b9268a3e8b833c19c1e4beb03f26db9a9dce31f6f7a19b5b1 + languageName: node + linkType: hard + +"@octokit/types@npm:^10.0.0": + version: 10.0.0 + resolution: "@octokit/types@npm:10.0.0" + dependencies: + "@octokit/openapi-types": ^18.0.0 + checksum: 8aafba2ff0cd2435fb70c291bf75ed071c0fa8a865cf6169648732068a35dec7b85a345851f18920ec5f3e94ee0e954988485caac0da09ec3f6781cc44fe153a + languageName: node + linkType: hard + +"@octokit/types@npm:^6.0.3, @octokit/types@npm:^6.16.1": version: 6.40.0 resolution: "@octokit/types@npm:6.40.0" dependencies: @@ -5398,6 +4706,15 @@ __metadata: languageName: node linkType: hard +"@octokit/types@npm:^9.0.0, @octokit/types@npm:^9.2.3": + version: 9.3.2 + resolution: "@octokit/types@npm:9.3.2" + dependencies: + "@octokit/openapi-types": ^18.0.0 + checksum: f55d096aaed3e04b8308d4422104fb888f355988056ba7b7ef0a4c397b8a3e54290d7827b06774dbe0c9ce55280b00db486286954f9c265aa6b03091026d9da8 + languageName: node + linkType: hard + "@open-draft/until@npm:^1.0.3": version: 1.0.3 resolution: "@open-draft/until@npm:1.0.3" @@ -6966,6 +6283,43 @@ __metadata: languageName: node linkType: hard +"@sigstore/bundle@npm:^1.1.0": + version: 1.1.0 + resolution: "@sigstore/bundle@npm:1.1.0" + dependencies: + "@sigstore/protobuf-specs": ^0.2.0 + checksum: 9bdd829f2867de6c03a19c5a7cff2c864887a9ed6e1c3438eb6659e838fde0b449fe83b1ca21efa00286a80c71e0144e20c0d9c415eead12e97d149285245c5a + languageName: node + linkType: hard + +"@sigstore/protobuf-specs@npm:^0.2.0": + version: 0.2.1 + resolution: "@sigstore/protobuf-specs@npm:0.2.1" + checksum: ddb7c829c7bf4148eccb571ede07cf9fda62f46b7b4d3a5ca02c0308c950ee90b4206b61082ee8d5753f24098632a8b24c147117bef8c68791bf5da537b55db9 + languageName: node + linkType: hard + +"@sigstore/sign@npm:^1.0.0": + version: 1.0.0 + resolution: "@sigstore/sign@npm:1.0.0" + dependencies: + "@sigstore/bundle": ^1.1.0 + "@sigstore/protobuf-specs": ^0.2.0 + make-fetch-happen: ^11.0.1 + checksum: cbdf409c39219d310f398e6a96b3ed7f422a58cfc0d8a40dd5b94996f805f189fdedf51afd559882bc18eb17054bf9d4f1a584b6af7b26c2f807636bceca5b19 + languageName: node + linkType: hard + +"@sigstore/tuf@npm:^1.0.3": + version: 1.0.3 + resolution: "@sigstore/tuf@npm:1.0.3" + dependencies: + "@sigstore/protobuf-specs": ^0.2.0 + tuf-js: ^1.1.7 + checksum: 0a32594b73ce3b3a4dfeec438ff98866a952a48ee6c020ddf57795062d9d328bc4327bb0e0c8d24011e3870c7d4670bc142a47025cbe7218c776f08084085421 + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -8490,6 +7844,23 @@ __metadata: languageName: node linkType: hard +"@tufjs/canonical-json@npm:1.0.0": + version: 1.0.0 + resolution: "@tufjs/canonical-json@npm:1.0.0" + checksum: 9ff3bcd12988fb23643690da3e009f9130b7b10974f8e7af4bd8ad230a228119de8609aa76d75264fe80f152b50872dea6ea53def69534436a4c24b4fcf6a447 + languageName: node + linkType: hard + +"@tufjs/models@npm:1.0.4": + version: 1.0.4 + resolution: "@tufjs/models@npm:1.0.4" + dependencies: + "@tufjs/canonical-json": 1.0.0 + minimatch: ^9.0.0 + checksum: b489baa854abce6865f360591c20d5eb7d8dde3fb150f42840c12bb7ee3e5e7a69eab9b2e44ea82ae1f8cd95b586963c5a5c5af8ba4ffa3614b3ddccbc306779 + languageName: node + linkType: hard + "@types/angular-route@npm:1.7.3": version: 1.7.3 resolution: "@types/angular-route@npm:1.7.3" @@ -10779,13 +10150,13 @@ __metadata: languageName: node linkType: hard -"@yarnpkg/parsers@npm:^3.0.0-rc.18": - version: 3.0.0-rc.22 - resolution: "@yarnpkg/parsers@npm:3.0.0-rc.22" +"@yarnpkg/parsers@npm:3.0.0-rc.46": + version: 3.0.0-rc.46 + resolution: "@yarnpkg/parsers@npm:3.0.0-rc.46" dependencies: js-yaml: ^3.10.0 tslib: ^2.4.0 - checksum: 4a31b4faad853b6cb09ff198017dd2f81782cb57ff8aaa2446ab9c8eb51aacaad3fa740e0c156c60c66cdb9cff8939f99b2b09c9890e2b8d015dcbed0150cb8a + checksum: 35dfd1b1ac7ed9babf231721eb90b58156e840e575f6792a8e5ab559beaed6e2d60833b857310e67d6282c9406357648df2f510e670ec37ef4bd41657f329a51 languageName: node linkType: hard @@ -10807,7 +10178,7 @@ __metadata: languageName: node linkType: hard -"JSONStream@npm:^1.0.4": +"JSONStream@npm:^1.3.5": version: 1.3.5 resolution: "JSONStream@npm:1.3.5" dependencies: @@ -11180,7 +10551,7 @@ __metadata: languageName: node linkType: hard -"aproba@npm:^1.0.3 || ^2.0.0, aproba@npm:^2.0.0": +"aproba@npm:^1.0.3 || ^2.0.0": version: 2.0.0 resolution: "aproba@npm:2.0.0" checksum: 5615cadcfb45289eea63f8afd064ab656006361020e1735112e346593856f87435e02d8dcc7ff0d11928bc7d425f27bc7c2a84f6c0b35ab0ff659c814c138a24 @@ -11415,13 +10786,6 @@ __metadata: languageName: node linkType: hard -"asap@npm:^2.0.0": - version: 2.0.6 - resolution: "asap@npm:2.0.6" - checksum: b296c92c4b969e973260e47523207cd5769abd27c245a68c26dc7a0fe8053c55bb04360237cb51cab1df52be939da77150ace99ad331fb7fb13b3423ed73ff3d - languageName: node - linkType: hard - "asap@npm:~1.0.0": version: 1.0.0 resolution: "asap@npm:1.0.0" @@ -11616,6 +10980,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.0.0": + version: 1.5.1 + resolution: "axios@npm:1.5.1" + dependencies: + follow-redirects: ^1.15.0 + form-data: ^4.0.0 + proxy-from-env: ^1.1.0 + checksum: 4444f06601f4ede154183767863d2b8e472b4a6bfc5253597ed6d21899887e1fd0ee2b3de792ac4f8459fe2e359d2aa07c216e45fd8b9e4e0688a6ebf48a5a8d + languageName: node + linkType: hard + "axobject-query@npm:^3.1.1": version: 3.1.1 resolution: "axobject-query@npm:3.1.1" @@ -11942,20 +11317,6 @@ __metadata: languageName: node linkType: hard -"bin-links@npm:^3.0.0": - version: 3.0.1 - resolution: "bin-links@npm:3.0.1" - dependencies: - cmd-shim: ^5.0.0 - mkdirp-infer-owner: ^2.0.0 - npm-normalize-package-bin: ^1.0.0 - read-cmd-shim: ^3.0.0 - rimraf: ^3.0.0 - write-file-atomic: ^4.0.0 - checksum: c608f0746c5851f259f7578ae5157d24fb019b00792d246bade6255136e5fbd41df43219a50d53f844c562afb6e41092a5f2b0be1bd890e08ff023d330327380 - languageName: node - linkType: hard - "binary-extensions@npm:^2.0.0": version: 2.2.0 resolution: "binary-extensions@npm:2.2.0" @@ -12254,10 +11615,10 @@ __metadata: languageName: node linkType: hard -"byte-size@npm:^7.0.0": - version: 7.0.1 - resolution: "byte-size@npm:7.0.1" - checksum: 6791663a6d53bf950e896f119d3648fe8d7e8ae677e2ccdae84d0e5b78f21126e25f9d73aa19be2a297cb27abd36b6f5c361c0de36ebb2f3eb8a853f2ac99a4a +"byte-size@npm:8.1.1": + version: 8.1.1 + resolution: "byte-size@npm:8.1.1" + checksum: 65f00881ffd3c2b282fe848ed954fa4ff8363eaa3f652102510668b90b3fad04d81889486ee1b641ee0d8c8b75cf32201f3b309e6b5fbb6cc869b48a91b62d3e languageName: node linkType: hard @@ -12323,7 +11684,7 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^16.0.0, cacache@npm:^16.0.6, cacache@npm:^16.1.0": +"cacache@npm:^16.1.0": version: 16.1.1 resolution: "cacache@npm:16.1.1" dependencies: @@ -12349,6 +11710,26 @@ __metadata: languageName: node linkType: hard +"cacache@npm:^17.0.0": + version: 17.1.4 + resolution: "cacache@npm:17.1.4" + dependencies: + "@npmcli/fs": ^3.1.0 + fs-minipass: ^3.0.0 + glob: ^10.2.2 + lru-cache: ^7.7.1 + minipass: ^7.0.3 + minipass-collect: ^1.0.2 + minipass-flush: ^1.0.5 + minipass-pipeline: ^1.2.4 + p-map: ^4.0.0 + ssri: ^10.0.0 + tar: ^6.1.11 + unique-filename: ^3.0.0 + checksum: b7751df756656954a51201335addced8f63fc53266fa56392c9f5ae83c8d27debffb4458ac2d168a744a4517ec3f2163af05c20097f93d17bdc2dc8a385e14a6 + languageName: node + linkType: hard + "cachedir@npm:^2.3.0": version: 2.3.0 resolution: "cachedir@npm:2.3.0" @@ -12619,7 +12000,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:3.5.3, chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": +"chokidar@npm:3.5.3, chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -12671,17 +12052,10 @@ __metadata: languageName: node linkType: hard -"ci-info@npm:^2.0.0": - version: 2.0.0 - resolution: "ci-info@npm:2.0.0" - checksum: 3b374666a85ea3ca43fa49aa3a048d21c9b475c96eb13c133505d2324e7ae5efd6a454f41efe46a152269e9b6a00c9edbe63ec7fa1921957165aae16625acd67 - languageName: node - linkType: hard - -"ci-info@npm:^3.2.0": - version: 3.2.0 - resolution: "ci-info@npm:3.2.0" - checksum: c68995a94e95ce3f233ff845e62dfc56f2e8ff1e3f5c1361bcdd520cbbc9726d8a54cbc1a685cb9ee19c3c5e71a1dade6dda23eb364b59b8e6c32508a9b761bc +"ci-info@npm:^3.2.0, ci-info@npm:^3.6.1": + version: 3.9.0 + resolution: "ci-info@npm:3.9.0" + checksum: 6b19dc9b2966d1f8c2041a838217299718f15d6c4b63ae36e4674edd2bee48f780e94761286a56aa59eb305a85fbea4ddffb7630ec063e7ec7e7e5ad42549a87 languageName: node linkType: hard @@ -12806,7 +12180,7 @@ __metadata: languageName: node linkType: hard -"clone-deep@npm:^4.0.1": +"clone-deep@npm:4.0.1, clone-deep@npm:^4.0.1": version: 4.0.1 resolution: "clone-deep@npm:4.0.1" dependencies: @@ -12863,12 +12237,10 @@ __metadata: languageName: node linkType: hard -"cmd-shim@npm:^5.0.0": - version: 5.0.0 - resolution: "cmd-shim@npm:5.0.0" - dependencies: - mkdirp-infer-owner: ^2.0.0 - checksum: 83d2a46cdf4adbb38d3d3184364b2df0e4c001ac770f5ca94373825d7a48838b4cb8a59534ef48f02b0d556caa047728589ca65c640c17c0b417b3afb34acfbb +"cmd-shim@npm:6.0.1": + version: 6.0.1 + resolution: "cmd-shim@npm:6.0.1" + checksum: 359006b3a5bb4a0ff161a44ccc18fbba947db748ef0dd12273e476792e316a5edb0945d74bfa1e91cd88ce0511025fde87901eda092c479d83cfcd6734562683 languageName: node linkType: hard @@ -12981,7 +12353,7 @@ __metadata: languageName: node linkType: hard -"columnify@npm:^1.6.0": +"columnify@npm:1.6.0": version: 1.6.0 resolution: "columnify@npm:1.6.0" dependencies: @@ -13098,13 +12470,6 @@ __metadata: languageName: node linkType: hard -"common-ancestor-path@npm:^1.0.1": - version: 1.0.1 - resolution: "common-ancestor-path@npm:1.0.1" - checksum: 1d2e4186067083d8cc413f00fc2908225f04ae4e19417ded67faa6494fb313c4fcd5b28a52326d1a62b466e2b3a4325e92c31133c5fee628cdf8856b3a57c3d7 - languageName: node - linkType: hard - "common-path-prefix@npm:^3.0.0": version: 3.0.0 resolution: "common-path-prefix@npm:3.0.0" @@ -13207,16 +12572,6 @@ __metadata: languageName: node linkType: hard -"config-chain@npm:^1.1.12": - version: 1.1.13 - resolution: "config-chain@npm:1.1.13" - dependencies: - ini: ^1.3.4 - proto-list: ~1.2.1 - checksum: 828137a28e7c2fc4b7fb229bd0cd6c1397bcf83434de54347e608154008f411749041ee392cbe42fab6307e02de4c12480260bf769b7d44b778fdea3839eafab - languageName: node - linkType: hard - "connect-history-api-fallback@npm:^2.0.0": version: 2.0.0 resolution: "connect-history-api-fallback@npm:2.0.0" @@ -13254,105 +12609,96 @@ __metadata: languageName: node linkType: hard -"conventional-changelog-angular@npm:^5.0.12": - version: 5.0.13 - resolution: "conventional-changelog-angular@npm:5.0.13" +"conventional-changelog-angular@npm:6.0.0": + version: 6.0.0 + resolution: "conventional-changelog-angular@npm:6.0.0" dependencies: compare-func: ^2.0.0 - q: ^1.5.1 - checksum: 6ed4972fce25a50f9f038c749cc9db501363131b0fb2efc1fccecba14e4b1c80651d0d758d4c350a609f32010c66fa343eefd49c02e79e911884be28f53f3f90 + checksum: ddc59ead53a45b817d83208200967f5340866782b8362d5e2e34105fdfa3d3a31585ebbdec7750bdb9de53da869f847e8ca96634a9801f51e27ecf4e7ffe2bad languageName: node linkType: hard -"conventional-changelog-core@npm:^4.2.4": - version: 4.2.4 - resolution: "conventional-changelog-core@npm:4.2.4" +"conventional-changelog-core@npm:5.0.1": + version: 5.0.1 + resolution: "conventional-changelog-core@npm:5.0.1" dependencies: add-stream: ^1.0.0 - conventional-changelog-writer: ^5.0.0 - conventional-commits-parser: ^3.2.0 - dateformat: ^3.0.0 - get-pkg-repo: ^4.0.0 - git-raw-commits: ^2.0.8 + conventional-changelog-writer: ^6.0.0 + conventional-commits-parser: ^4.0.0 + dateformat: ^3.0.3 + get-pkg-repo: ^4.2.1 + git-raw-commits: ^3.0.0 git-remote-origin-url: ^2.0.0 - git-semver-tags: ^4.1.1 - lodash: ^4.17.15 - normalize-package-data: ^3.0.0 - q: ^1.5.1 + git-semver-tags: ^5.0.0 + normalize-package-data: ^3.0.3 read-pkg: ^3.0.0 read-pkg-up: ^3.0.0 - through2: ^4.0.0 - checksum: 56d5194040495ea316e53fd64cb3614462c318f0fe54b1bf25aba6fba9b3d51cb9fdf7ac5b766f17e5529a3f90e317257394e00b0a9a5ce42caf3a59f82afb3a + checksum: 5f37f14f8d5effb4c6bf861df11e918a277ecc2cf94534eaed44d1455b11ef450d0f6d122f0e7450a44a268d9473730cf918b7558964dcba2f0ac0896824e66f languageName: node linkType: hard -"conventional-changelog-preset-loader@npm:^2.3.4": - version: 2.3.4 - resolution: "conventional-changelog-preset-loader@npm:2.3.4" - checksum: 23a889b7fcf6fe7653e61f32a048877b2f954dcc1e0daa2848c5422eb908e6f24c78372f8d0d2130b5ed941c02e7010c599dccf44b8552602c6c8db9cb227453 +"conventional-changelog-preset-loader@npm:^3.0.0": + version: 3.0.0 + resolution: "conventional-changelog-preset-loader@npm:3.0.0" + checksum: 199c4730c5151f243d35c24585114900c2a7091eab5832cfeb49067a18a2b77d5c9a86b779e6e18b49278a1ff83c011c1d9bb6da95bd1f78d9e36d4d379216d5 languageName: node linkType: hard -"conventional-changelog-writer@npm:^5.0.0": - version: 5.0.1 - resolution: "conventional-changelog-writer@npm:5.0.1" +"conventional-changelog-writer@npm:^6.0.0": + version: 6.0.1 + resolution: "conventional-changelog-writer@npm:6.0.1" dependencies: - conventional-commits-filter: ^2.0.7 - dateformat: ^3.0.0 + conventional-commits-filter: ^3.0.0 + dateformat: ^3.0.3 handlebars: ^4.7.7 json-stringify-safe: ^5.0.1 - lodash: ^4.17.15 - meow: ^8.0.0 - semver: ^6.0.0 - split: ^1.0.0 - through2: ^4.0.0 + meow: ^8.1.2 + semver: ^7.0.0 + split: ^1.0.1 bin: conventional-changelog-writer: cli.js - checksum: 5c0129db44577f14b1f8de225b62a392a9927ba7fe3422cb21ad71a771b8472bd03badb7c87cb47419913abc3f2ce3759b69f59550cdc6f7a7b0459015b3b44c + checksum: d8619ff7446efa71e0a019c07bdf20debff3f32438f783277b80314109429d7075b3d913e59c57cd6e014e9bef611c2a8fb052de2832144f38c0e54485257126 languageName: node linkType: hard -"conventional-commits-filter@npm:^2.0.7": - version: 2.0.7 - resolution: "conventional-commits-filter@npm:2.0.7" +"conventional-commits-filter@npm:^3.0.0": + version: 3.0.0 + resolution: "conventional-commits-filter@npm:3.0.0" dependencies: lodash.ismatch: ^4.4.0 - modify-values: ^1.0.0 - checksum: feb567f680a6da1baaa1ef3cff393b3c56a5828f77ab9df5e70626475425d109a6fee0289b4979223c62bbd63bf9c98ef532baa6fcb1b66ee8b5f49077f5d46c + modify-values: ^1.0.1 + checksum: 73337f42acff7189e1dfca8d13c9448ce085ac1c09976cb33617cc909949621befb1640b1c6c30a1be4953a1be0deea9e93fa0dc86725b8be8e249a64fbb4632 languageName: node linkType: hard -"conventional-commits-parser@npm:^3.2.0": - version: 3.2.4 - resolution: "conventional-commits-parser@npm:3.2.4" +"conventional-commits-parser@npm:^4.0.0": + version: 4.0.0 + resolution: "conventional-commits-parser@npm:4.0.0" dependencies: - JSONStream: ^1.0.4 + JSONStream: ^1.3.5 is-text-path: ^1.0.1 - lodash: ^4.17.15 - meow: ^8.0.0 - split2: ^3.0.0 - through2: ^4.0.0 + meow: ^8.1.2 + split2: ^3.2.2 bin: conventional-commits-parser: cli.js - checksum: 1627ff203bc9586d89e47a7fe63acecf339aba74903b9114e23d28094f79d4e2d6389bf146ae561461dcba8fc42e7bc228165d2b173f15756c43f1d32bc50bfd + checksum: 12d95b5ba8e0710a6d3cd2e01f01dd7818fdf0bb2b33f4b75444e2c9aee49598776b0706a528ed49e83aec5f1896c32cbc7f8e6589f61a15187293707448f928 languageName: node linkType: hard -"conventional-recommended-bump@npm:^6.1.0": - version: 6.1.0 - resolution: "conventional-recommended-bump@npm:6.1.0" +"conventional-recommended-bump@npm:7.0.1": + version: 7.0.1 + resolution: "conventional-recommended-bump@npm:7.0.1" dependencies: concat-stream: ^2.0.0 - conventional-changelog-preset-loader: ^2.3.4 - conventional-commits-filter: ^2.0.7 - conventional-commits-parser: ^3.2.0 - git-raw-commits: ^2.0.8 - git-semver-tags: ^4.1.1 - meow: ^8.0.0 - q: ^1.5.1 + conventional-changelog-preset-loader: ^3.0.0 + conventional-commits-filter: ^3.0.0 + conventional-commits-parser: ^4.0.0 + git-raw-commits: ^3.0.0 + git-semver-tags: ^5.0.0 + meow: ^8.1.2 bin: conventional-recommended-bump: cli.js - checksum: da1d7a5f3b9f7706bede685cdcb3db67997fdaa43c310fd5bf340955c84a4b85dbb9427031522ee06dad290b730a54be987b08629d79c73720dbad3a2531146b + checksum: e2d1f2f40f93612a6da035d0c1a12d70208e0da509a17a9c9296a05e73a6eca5d81fe8c6a7b45e973181fa7c876c6edb9a114a2d7da4f6df00c47c7684ab62d2 languageName: node linkType: hard @@ -14405,7 +13751,7 @@ __metadata: languageName: node linkType: hard -"dateformat@npm:^3.0.0": +"dateformat@npm:^3.0.3": version: 3.0.3 resolution: "dateformat@npm:3.0.3" checksum: ca4911148abb09887bd9bdcd632c399b06f3ecad709a18eb594d289a1031982f441e08e281db77ffebcb2cbcbfa1ac578a7cbfbf8743f41009aa5adc1846ed34 @@ -14477,13 +13823,6 @@ __metadata: languageName: node linkType: hard -"debuglog@npm:^1.0.1": - version: 1.0.1 - resolution: "debuglog@npm:1.0.1" - checksum: 970679f2eb7a73867e04d45b52583e7ec6dee1f33c058e9147702e72a665a9647f9c3d6e7c2f66f6bf18510b23eb5ded1b617e48ac1db23603809c5ddbbb9763 - languageName: node - linkType: hard - "decamelize-keys@npm:^1.1.0": version: 1.1.0 resolution: "decamelize-keys@npm:1.1.0" @@ -14529,7 +13868,7 @@ __metadata: languageName: node linkType: hard -"dedent@npm:^0.7.0": +"dedent@npm:0.7.0, dedent@npm:^0.7.0": version: 0.7.0 resolution: "dedent@npm:0.7.0" checksum: 87de191050d9a40dd70cad01159a0bcf05ecb59750951242070b6abf9569088684880d00ba92a955b4058804f16eeaf91d604f283929b4f614d181cd7ae633d2 @@ -14697,7 +14036,7 @@ __metadata: languageName: node linkType: hard -"deprecation@npm:^2.0.0, deprecation@npm:^2.3.1": +"deprecation@npm:^2.0.0": version: 2.3.1 resolution: "deprecation@npm:2.3.1" checksum: f56a05e182c2c195071385455956b0c4106fe14e36245b00c689ceef8e8ab639235176a96977ba7c74afb173317fac2e0ec6ec7a1c6d1e6eaa401c586c714132 @@ -14725,7 +14064,7 @@ __metadata: languageName: node linkType: hard -"detect-indent@npm:^6.0.0, detect-indent@npm:^6.1.0": +"detect-indent@npm:^6.1.0": version: 6.1.0 resolution: "detect-indent@npm:6.1.0" checksum: ab953a73c72dbd4e8fc68e4ed4bfd92c97eb6c43734af3900add963fd3a9316f3bc0578b018b24198d4c31a358571eff5f0656e81a1f3b9ad5c547d58b2d093d @@ -14789,16 +14128,6 @@ __metadata: languageName: node linkType: hard -"dezalgo@npm:^1.0.0": - version: 1.0.4 - resolution: "dezalgo@npm:1.0.4" - dependencies: - asap: ^2.0.0 - wrappy: 1 - checksum: 895389c6aead740d2ab5da4d3466d20fa30f738010a4d3f4dcccc9fc645ca31c9d10b7e1804ae489b1eb02c7986f9f1f34ba132d409b043082a86d9a4e745624 - languageName: node - linkType: hard - "diff-sequences@npm:^26.6.2": version: 26.6.2 resolution: "diff-sequences@npm:26.6.2" @@ -15048,33 +14377,17 @@ __metadata: languageName: node linkType: hard -"dot-prop@npm:^6.0.1": - version: 6.0.1 - resolution: "dot-prop@npm:6.0.1" - dependencies: - is-obj: ^2.0.0 - checksum: 0f47600a4b93e1dc37261da4e6909652c008832a5d3684b5bf9a9a0d3f4c67ea949a86dceed9b72f5733ed8e8e6383cc5958df3bbd0799ee317fd181f2ece700 - languageName: node - linkType: hard - -"dotenv-expand@npm:^10.0.0": +"dotenv-expand@npm:^10.0.0, dotenv-expand@npm:~10.0.0": version: 10.0.0 resolution: "dotenv-expand@npm:10.0.0" checksum: 2a38b470efe0abcb1ac8490421a55e1d764dc9440fd220942bce40965074f3fb00b585f4346020cb0f0f219966ee6b4ee5023458b3e2953fe5b3214de1b314ee languageName: node linkType: hard -"dotenv@npm:^16.0.0": - version: 16.0.3 - resolution: "dotenv@npm:16.0.3" - checksum: afcf03f373d7a6d62c7e9afea6328e62851d627a4e73f2e12d0a8deae1cd375892004f3021883f8aec85932cd2834b091f568ced92b4774625b321db83b827f8 - languageName: node - linkType: hard - -"dotenv@npm:~10.0.0": - version: 10.0.0 - resolution: "dotenv@npm:10.0.0" - checksum: f412c5fe8c24fbe313d302d2500e247ba8a1946492db405a4de4d30dd0eb186a88a43f13c958c5a7de303938949c4231c56994f97d05c4bc1f22478d631b4005 +"dotenv@npm:^16.0.0, dotenv@npm:~16.3.1": + version: 16.3.1 + resolution: "dotenv@npm:16.3.1" + checksum: 15d75e7279018f4bafd0ee9706593dd14455ddb71b3bcba9c52574460b7ccaf67d5cf8b2c08a5af1a9da6db36c956a04a1192b101ee102a3e0cf8817bbcf3dfd languageName: node linkType: hard @@ -15128,7 +14441,7 @@ __metadata: languageName: node linkType: hard -"ejs@npm:^3.1.8": +"ejs@npm:^3.1.7, ejs@npm:^3.1.8": version: 3.1.9 resolution: "ejs@npm:3.1.9" dependencies: @@ -15292,7 +14605,7 @@ __metadata: languageName: node linkType: hard -"envinfo@npm:^7.7.3, envinfo@npm:^7.7.4": +"envinfo@npm:7.8.1, envinfo@npm:^7.7.3": version: 7.8.1 resolution: "envinfo@npm:7.8.1" bin: @@ -16352,6 +15665,23 @@ __metadata: languageName: node linkType: hard +"execa@npm:5.0.0": + version: 5.0.0 + resolution: "execa@npm:5.0.0" + dependencies: + cross-spawn: ^7.0.3 + get-stream: ^6.0.0 + human-signals: ^2.1.0 + is-stream: ^2.0.0 + merge-stream: ^2.0.0 + npm-run-path: ^4.0.1 + onetime: ^5.1.2 + signal-exit: ^3.0.3 + strip-final-newline: ^2.0.0 + checksum: a044367ebdcc68ca019810cb134510fc77bbc55c799122258ee0e00e289c132941ab48c2a331a036699c42bc8d479d451ae67c105fce5ce5cc813e7dd92d642b + languageName: node + linkType: hard + "execa@npm:5.1.1, execa@npm:^5.0.0, execa@npm:^5.1.1": version: 5.1.1 resolution: "execa@npm:5.1.1" @@ -16543,19 +15873,6 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:3.2.7": - version: 3.2.7 - resolution: "fast-glob@npm:3.2.7" - dependencies: - "@nodelib/fs.stat": ^2.0.2 - "@nodelib/fs.walk": ^1.2.3 - glob-parent: ^5.1.2 - merge2: ^1.3.0 - micromatch: ^4.0.4 - checksum: 2f4708ff112d2b451888129fdd9a0938db88b105b0ddfd043c064e3c4d3e20eed8d7c7615f7565fee660db34ddcf08a2db1bf0ab3c00b87608e4719694642d78 - languageName: node - linkType: hard - "fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.1": version: 3.3.1 resolution: "fast-glob@npm:3.3.1" @@ -16915,13 +16232,13 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.14.9": - version: 1.15.2 - resolution: "follow-redirects@npm:1.15.2" +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.14.9, follow-redirects@npm:^1.15.0": + version: 1.15.3 + resolution: "follow-redirects@npm:1.15.3" peerDependenciesMeta: debug: optional: true - checksum: faa66059b66358ba65c234c2f2a37fcec029dc22775f35d9ad6abac56003268baf41e55f9ee645957b32c7d9f62baf1f0b906e68267276f54ec4b4c597c2b190 + checksum: 584da22ec5420c837bd096559ebfb8fe69d82512d5585004e36a3b4a6ef6d5905780e0c74508c7b72f907d1fa2b7bd339e613859e9c304d0dc96af2027fd0231 languageName: node linkType: hard @@ -17059,7 +16376,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:11.1.1, fs-extra@npm:^11.1.0": +"fs-extra@npm:11.1.1, fs-extra@npm:^11.1.0, fs-extra@npm:^11.1.1": version: 11.1.1 resolution: "fs-extra@npm:11.1.1" dependencies: @@ -17070,7 +16387,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^10.0.0, fs-extra@npm:^10.1.0": +"fs-extra@npm:^10.0.0": version: 10.1.0 resolution: "fs-extra@npm:10.1.0" dependencies: @@ -17126,6 +16443,15 @@ __metadata: languageName: node linkType: hard +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: ^7.0.3 + checksum: 8722a41109130851d979222d3ec88aabaceeaaf8f57b2a8f744ef8bd2d1ce95453b04a61daa0078822bc5cd21e008814f06fe6586f56fef511e71b8d2394d802 + languageName: node + linkType: hard + "fs-mkdirp-stream@npm:^1.0.0": version: 1.0.0 resolution: "fs-mkdirp-stream@npm:1.0.0" @@ -17319,7 +16645,7 @@ __metadata: languageName: node linkType: hard -"get-pkg-repo@npm:^4.0.0": +"get-pkg-repo@npm:^4.2.1": version: 4.2.1 resolution: "get-pkg-repo@npm:4.2.1" dependencies: @@ -17333,13 +16659,20 @@ __metadata: languageName: node linkType: hard -"get-port@npm:^5.1.1": +"get-port@npm:5.1.1, get-port@npm:^5.1.1": version: 5.1.1 resolution: "get-port@npm:5.1.1" checksum: 0162663ffe5c09e748cd79d97b74cd70e5a5c84b760a475ce5767b357fb2a57cb821cee412d646aa8a156ed39b78aab88974eddaa9e5ee926173c036c0713787 languageName: node linkType: hard +"get-stream@npm:6.0.0": + version: 6.0.0 + resolution: "get-stream@npm:6.0.0" + checksum: 587e6a93127f9991b494a566f4971cf7a2645dfa78034818143480a80587027bdd8826cdcf80d0eff4a4a19de0d231d157280f24789fc9cc31492e1dcc1290cf + languageName: node + linkType: hard + "get-stream@npm:^5.0.0, get-stream@npm:^5.1.0": version: 5.2.0 resolution: "get-stream@npm:5.2.0" @@ -17429,18 +16762,16 @@ __metadata: languageName: node linkType: hard -"git-raw-commits@npm:^2.0.8": - version: 2.0.11 - resolution: "git-raw-commits@npm:2.0.11" +"git-raw-commits@npm:^3.0.0": + version: 3.0.0 + resolution: "git-raw-commits@npm:3.0.0" dependencies: dargs: ^7.0.0 - lodash: ^4.17.15 - meow: ^8.0.0 - split2: ^3.0.0 - through2: ^4.0.0 + meow: ^8.1.2 + split2: ^3.2.2 bin: git-raw-commits: cli.js - checksum: c178af43633684106179793b6e3473e1d2bb50bb41d04e2e285ea4eef342ca4090fee6bc8a737552fde879d22346c90de5c49f18c719a0f38d4c934f258a0f79 + checksum: 198892f307829d22fc8ec1c9b4a63876a1fde847763857bb74bd1b04c6f6bc0d7464340c25d0f34fd0fb395759363aa1f8ce324357027320d80523bf234676ab languageName: node linkType: hard @@ -17454,15 +16785,15 @@ __metadata: languageName: node linkType: hard -"git-semver-tags@npm:^4.1.1": - version: 4.1.1 - resolution: "git-semver-tags@npm:4.1.1" +"git-semver-tags@npm:^5.0.0": + version: 5.0.1 + resolution: "git-semver-tags@npm:5.0.1" dependencies: - meow: ^8.0.0 - semver: ^6.0.0 + meow: ^8.1.2 + semver: ^7.0.0 bin: git-semver-tags: cli.js - checksum: e16d02a515c0f88289a28b5bf59bf42c0dc053765922d3b617ae4b50546bd4f74a25bf3ad53b91cb6c1159319a2e92533b160c573b856c2629125c8b26b3b0e3 + checksum: c181e1d9e7649fd90e6c347f400f791db08b236265d79874dfa60f09ca893fa7a4fceebf3fd5f01443705e7eac5c73c5235eb96c6bc4a39eb37746a1d7c49ec4 languageName: node linkType: hard @@ -17476,7 +16807,7 @@ __metadata: languageName: node linkType: hard -"git-url-parse@npm:^13.1.0": +"git-url-parse@npm:13.1.0": version: 13.1.0 resolution: "git-url-parse@npm:13.1.0" dependencies: @@ -17501,6 +16832,15 @@ __metadata: languageName: node linkType: hard +"glob-parent@npm:5.1.2, glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": + version: 5.1.2 + resolution: "glob-parent@npm:5.1.2" + dependencies: + is-glob: ^4.0.1 + checksum: f4f2bfe2425296e8a47e36864e4f42be38a996db40420fe434565e4480e3322f18eb37589617a98640c5dc8fdec1a387007ee18dbb1f3f5553409c34d17f425e + languageName: node + linkType: hard + "glob-parent@npm:^3.1.0": version: 3.1.0 resolution: "glob-parent@npm:3.1.0" @@ -17511,15 +16851,6 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^5.1.1, glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": - version: 5.1.2 - resolution: "glob-parent@npm:5.1.2" - dependencies: - is-glob: ^4.0.1 - checksum: f4f2bfe2425296e8a47e36864e4f42be38a996db40420fe434565e4480e3322f18eb37589617a98640c5dc8fdec1a387007ee18dbb1f3f5553409c34d17f425e - languageName: node - linkType: hard - "glob-parent@npm:^6.0.1, glob-parent@npm:^6.0.2": version: 6.0.2 resolution: "glob-parent@npm:6.0.2" @@ -17597,7 +16928,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.0.0, glob@npm:^10.2.5, glob@npm:^10.2.7": +"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.2.5, glob@npm:^10.2.7": version: 10.3.10 resolution: "glob@npm:10.3.10" dependencies: @@ -17639,6 +16970,18 @@ __metadata: languageName: node linkType: hard +"glob@npm:^9.2.0": + version: 9.3.5 + resolution: "glob@npm:9.3.5" + dependencies: + fs.realpath: ^1.0.0 + minimatch: ^8.0.2 + minipass: ^4.2.4 + path-scurry: ^1.6.1 + checksum: 94b093adbc591bc36b582f77927d1fb0dbf3ccc231828512b017601408be98d1fe798fc8c0b19c6f2d1a7660339c3502ce698de475e9d938ccbb69b47b647c84 + languageName: node + linkType: hard + "global-dirs@npm:^3.0.0": version: 3.0.1 resolution: "global-dirs@npm:3.0.1" @@ -17709,7 +17052,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:^11.0.1, globby@npm:^11.0.2, globby@npm:^11.1.0": +"globby@npm:11.1.0, globby@npm:^11.0.1, globby@npm:^11.0.2, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -17752,10 +17095,10 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.0.0, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": - version: 4.2.10 - resolution: "graceful-fs@npm:4.2.10" - checksum: 3f109d70ae123951905d85032ebeae3c2a5a7a997430df00ea30df0e3a6c60cf6689b109654d6fdacd28810a053348c4d14642da1d075049e6be1ba5216218da +"graceful-fs@npm:4.2.11, graceful-fs@npm:^4.0.0, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 languageName: node linkType: hard @@ -17995,7 +17338,7 @@ __metadata: json-source-map: 0.6.1 jsurl: ^0.1.5 kbar: 0.1.0-beta.40 - lerna: 5.5.4 + lerna: 7.4.1 lodash: 4.17.21 logfmt: ^1.3.2 lru-cache: 10.0.0 @@ -18293,7 +17636,7 @@ __metadata: languageName: node linkType: hard -"has-unicode@npm:^2.0.0, has-unicode@npm:^2.0.1": +"has-unicode@npm:2.0.1, has-unicode@npm:^2.0.0, has-unicode@npm:^2.0.1": version: 2.0.1 resolution: "has-unicode@npm:2.0.1" checksum: 1eab07a7436512db0be40a710b29b5dc21fa04880b7f63c9980b706683127e3c1b57cb80ea96d47991bdae2dfe479604f6a1ba410106ee1046a41d1bd0814400 @@ -18458,12 +17801,12 @@ __metadata: languageName: node linkType: hard -"hosted-git-info@npm:^5.0.0": - version: 5.0.0 - resolution: "hosted-git-info@npm:5.0.0" +"hosted-git-info@npm:^6.0.0": + version: 6.1.1 + resolution: "hosted-git-info@npm:6.1.1" dependencies: lru-cache: ^7.5.1 - checksum: 515e69463d123635f70d70656c5ec648951ffc1987f92a87cb4a038e1794bfed833cf87569b358b137ebbc75d992c073ed0408d420c9e5b717c2b4f0a291490c + checksum: fcd3ca2eaa05f3201425ccbb8aa47f88cdda4a3a6d79453f8e269f7171356278bd1db08f059d8439eb5eaa91c6a8a20800fc49cca6e9e4e899b202a332d5ba6b languageName: node linkType: hard @@ -18613,7 +17956,7 @@ __metadata: languageName: node linkType: hard -"http-cache-semantics@npm:^4.1.0": +"http-cache-semantics@npm:^4.1.0, http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" checksum: 83ac0bc60b17a3a36f9953e7be55e5c8f41acc61b22583060e8dedc9dd5e3607c823a88d0926f9150e571f90946835c7fe150732801010845c72cd8bbff1a236 @@ -18903,6 +18246,15 @@ __metadata: languageName: node linkType: hard +"ignore-walk@npm:^6.0.0": + version: 6.0.3 + resolution: "ignore-walk@npm:6.0.3" + dependencies: + minimatch: ^9.0.0 + checksum: d8ba534beb3a3fa48ddd32c79bbedb14a831ff7fab548674765d661d8f8d0df4b0827e3ad86e35cb15ff027655bfd6a477bd8d5d0411e229975a7c716f1fc9de + languageName: node + linkType: hard + "ignore@npm:^3.3.10": version: 3.3.10 resolution: "ignore@npm:3.3.10" @@ -18962,15 +18314,15 @@ __metadata: languageName: node linkType: hard -"import-local@npm:^3.0.2": - version: 3.0.3 - resolution: "import-local@npm:3.0.3" +"import-local@npm:3.1.0, import-local@npm:^3.0.2": + version: 3.1.0 + resolution: "import-local@npm:3.1.0" dependencies: pkg-dir: ^4.2.0 resolve-cwd: ^3.0.0 bin: import-local-fixture: fixtures/cli.js - checksum: 38ae57d35e7fd5f63b55895050c798d4dd590e4e2337e9ffa882fb3ea7a7716f3162c7300e382e0a733ca5d07b389fadff652c00fa7b072d5cb6ea34ca06b179 + checksum: bfcdb63b5e3c0e245e347f3107564035b128a414c4da1172a20dc67db2504e05ede4ac2eee1252359f78b0bfd7b19ef180aec427c2fce6493ae782d73a04cddd languageName: node linkType: hard @@ -19033,25 +18385,25 @@ __metadata: languageName: node linkType: hard -"ini@npm:^1.3.2, ini@npm:^1.3.4, ini@npm:^1.3.5": +"ini@npm:^1.3.2, ini@npm:^1.3.5, ini@npm:^1.3.8": version: 1.3.8 resolution: "ini@npm:1.3.8" checksum: dfd98b0ca3a4fc1e323e38a6c8eb8936e31a97a918d3b377649ea15bdb15d481207a0dda1021efbd86b464cae29a0d33c1d7dcaf6c5672bee17fa849bc50a1b3 languageName: node linkType: hard -"init-package-json@npm:^3.0.2": - version: 3.0.2 - resolution: "init-package-json@npm:3.0.2" +"init-package-json@npm:5.0.0": + version: 5.0.0 + resolution: "init-package-json@npm:5.0.0" dependencies: - npm-package-arg: ^9.0.1 - promzard: ^0.3.0 - read: ^1.0.7 - read-package-json: ^5.0.0 + npm-package-arg: ^10.0.0 + promzard: ^1.0.0 + read: ^2.0.0 + read-package-json: ^6.0.0 semver: ^7.3.5 validate-npm-package-license: ^3.0.4 - validate-npm-package-name: ^4.0.0 - checksum: e027f60e4a1564809eee790d5a842341c784888fd7c7ace5f9a34ea76224c0adb6f3ab3bf205cf1c9c877a6e1a76c68b00847a984139f60813125d7b42a23a13 + validate-npm-package-name: ^5.0.0 + checksum: ad601c717d5ea3ff5a416cbe7d39417bb3914596dce7a386bffe856229435ebef06eb600736326effdd4e57a02d41164aa525d31d51ec49812c8e8c215d1d7c8 languageName: node linkType: hard @@ -19283,18 +18635,7 @@ __metadata: languageName: node linkType: hard -"is-ci@npm:^2.0.0": - version: 2.0.0 - resolution: "is-ci@npm:2.0.0" - dependencies: - ci-info: ^2.0.0 - bin: - is-ci: bin.js - checksum: 77b869057510f3efa439bbb36e9be429d53b3f51abd4776eeea79ab3b221337fe1753d1e50058a9e2c650d38246108beffb15ccfd443929d77748d8c0cc90144 - languageName: node - linkType: hard - -"is-ci@npm:^3.0.0": +"is-ci@npm:3.0.1, is-ci@npm:^3.0.0": version: 3.0.1 resolution: "is-ci@npm:3.0.1" dependencies: @@ -19567,7 +18908,7 @@ __metadata: languageName: node linkType: hard -"is-plain-obj@npm:^2.0.0, is-plain-obj@npm:^2.1.0": +"is-plain-obj@npm:^2.1.0": version: 2.1.0 resolution: "is-plain-obj@npm:2.1.0" checksum: cec9100678b0a9fe0248a81743041ed990c2d4c99f893d935545cfbc42876cbe86d207f3b895700c690ad2fa520e568c44afc1605044b535a7820c1d40e38daa @@ -19671,6 +19012,13 @@ __metadata: languageName: node linkType: hard +"is-stream@npm:2.0.0": + version: 2.0.0 + resolution: "is-stream@npm:2.0.0" + checksum: 4dc47738e26bc4f1b3be9070b6b9e39631144f204fc6f87db56961220add87c10a999ba26cf81699f9ef9610426f69cb08a4713feff8deb7d8cadac907826935 + languageName: node + linkType: hard + "is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" @@ -19714,7 +19062,7 @@ __metadata: languageName: node linkType: hard -"is-typedarray@npm:^1.0.0, is-typedarray@npm:~1.0.0": +"is-typedarray@npm:~1.0.0": version: 1.0.0 resolution: "is-typedarray@npm:1.0.0" checksum: 3508c6cd0a9ee2e0df2fa2e9baabcdc89e911c7bd5cf64604586697212feec525aa21050e48affb5ffc3df20f0f5d2e2cf79b08caa64e1ccc9578e251763aef7 @@ -20074,6 +19422,18 @@ __metadata: languageName: node linkType: hard +"jest-diff@npm:>=29.4.3 < 30, jest-diff@npm:^29.4.1, jest-diff@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-diff@npm:29.7.0" + dependencies: + chalk: ^4.0.0 + diff-sequences: ^29.6.3 + jest-get-type: ^29.6.3 + pretty-format: ^29.7.0 + checksum: 08e24a9dd43bfba1ef07a6374e5af138f53137b79ec3d5cc71a2303515335898888fa5409959172e1e05de966c9e714368d15e8994b0af7441f0721ee8e1bb77 + languageName: node + linkType: hard + "jest-diff@npm:^26.0.0": version: 26.6.2 resolution: "jest-diff@npm:26.6.2" @@ -20098,18 +19458,6 @@ __metadata: languageName: node linkType: hard -"jest-diff@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-diff@npm:29.7.0" - dependencies: - chalk: ^4.0.0 - diff-sequences: ^29.6.3 - jest-get-type: ^29.6.3 - pretty-format: ^29.7.0 - checksum: 08e24a9dd43bfba1ef07a6374e5af138f53137b79ec3d5cc71a2303515335898888fa5409959172e1e05de966c9e714368d15e8994b0af7441f0721ee8e1bb77 - languageName: node - linkType: hard - "jest-docblock@npm:^29.7.0": version: 29.7.0 resolution: "jest-docblock@npm:29.7.0" @@ -20769,6 +20117,13 @@ __metadata: languageName: node linkType: hard +"json-parse-even-better-errors@npm:^3.0.0": + version: 3.0.0 + resolution: "json-parse-even-better-errors@npm:3.0.0" + checksum: f1970b5220c7fa23d888565510752c3d5e863f93668a202fcaa719739fa41485dfc6a1db212f702ebd3c873851cc067aebc2917e3f79763cae2fdb95046f38f3 + languageName: node + linkType: hard + "json-schema-traverse@npm:^0.4.1": version: 0.4.1 resolution: "json-schema-traverse@npm:0.4.1" @@ -20804,13 +20159,6 @@ __metadata: languageName: node linkType: hard -"json-stringify-nice@npm:^1.1.4": - version: 1.1.4 - resolution: "json-stringify-nice@npm:1.1.4" - checksum: 6ddf781148b46857ab04e97f47be05f14c4304b86eb5478369edbeacd070c21c697269964b982fc977e8989d4c59091103b1d9dc291aba40096d6cbb9a392b72 - languageName: node - linkType: hard - "json-stringify-pretty-compact@npm:^2.0.0": version: 2.0.0 resolution: "json-stringify-pretty-compact@npm:2.0.0" @@ -20836,7 +20184,7 @@ __metadata: languageName: node linkType: hard -"json5@npm:^2.1.2, json5@npm:^2.2.3": +"json5@npm:^2.1.2, json5@npm:^2.2.2, json5@npm:^2.2.3": version: 2.2.3 resolution: "json5@npm:2.2.3" bin: @@ -20950,20 +20298,6 @@ __metadata: languageName: node linkType: hard -"just-diff-apply@npm:^5.2.0": - version: 5.3.1 - resolution: "just-diff-apply@npm:5.3.1" - checksum: c864606096f2506f043f90c58196bf47344b4c60e97171ea6ec3430e4664aa2eddc6722ff87c66fef4d6d6b47364b053f90a10d59319135a6c06ba5dd424b58e - languageName: node - linkType: hard - -"just-diff@npm:^5.0.1": - version: 5.0.3 - resolution: "just-diff@npm:5.0.3" - checksum: 89e5c3deb0525e8d5f651a0775ca62807d8924e386c3ab58d81ac7392ac10f6c98c677ea6e5578618e483fc88139e7ebde1c4130296e83d802ac3103f7e210cd - languageName: node - linkType: hard - "kbar@npm:0.1.0-beta.40": version: 0.1.0-beta.40 resolution: "kbar@npm:0.1.0-beta.40" @@ -21104,33 +20438,88 @@ __metadata: languageName: node linkType: hard -"lerna@npm:5.5.4": - version: 5.5.4 - resolution: "lerna@npm:5.5.4" +"lerna@npm:7.4.1": + version: 7.4.1 + resolution: "lerna@npm:7.4.1" dependencies: - "@lerna/add": 5.5.4 - "@lerna/bootstrap": 5.5.4 - "@lerna/changed": 5.5.4 - "@lerna/clean": 5.5.4 - "@lerna/cli": 5.5.4 - "@lerna/create": 5.5.4 - "@lerna/diff": 5.5.4 - "@lerna/exec": 5.5.4 - "@lerna/import": 5.5.4 - "@lerna/info": 5.5.4 - "@lerna/init": 5.5.4 - "@lerna/link": 5.5.4 - "@lerna/list": 5.5.4 - "@lerna/publish": 5.5.4 - "@lerna/run": 5.5.4 - "@lerna/version": 5.5.4 - import-local: ^3.0.2 + "@lerna/child-process": 7.4.1 + "@lerna/create": 7.4.1 + "@npmcli/run-script": 6.0.2 + "@nx/devkit": ">=16.5.1 < 17" + "@octokit/plugin-enterprise-rest": 6.0.1 + "@octokit/rest": 19.0.11 + byte-size: 8.1.1 + chalk: 4.1.0 + clone-deep: 4.0.1 + cmd-shim: 6.0.1 + columnify: 1.6.0 + conventional-changelog-angular: 6.0.0 + conventional-changelog-core: 5.0.1 + conventional-recommended-bump: 7.0.1 + cosmiconfig: ^8.2.0 + dedent: 0.7.0 + envinfo: 7.8.1 + execa: 5.0.0 + fs-extra: ^11.1.1 + get-port: 5.1.1 + get-stream: 6.0.0 + git-url-parse: 13.1.0 + glob-parent: 5.1.2 + globby: 11.1.0 + graceful-fs: 4.2.11 + has-unicode: 2.0.1 + import-local: 3.1.0 + ini: ^1.3.8 + init-package-json: 5.0.0 + inquirer: ^8.2.4 + is-ci: 3.0.1 + is-stream: 2.0.0 + jest-diff: ">=29.4.3 < 30" + js-yaml: 4.1.0 + libnpmaccess: 7.0.2 + libnpmpublish: 7.3.0 + load-json-file: 6.2.0 + lodash: ^4.17.21 + make-dir: 4.0.0 + minimatch: 3.0.5 + multimatch: 5.0.0 + node-fetch: 2.6.7 + npm-package-arg: 8.1.1 + npm-packlist: 5.1.1 + npm-registry-fetch: ^14.0.5 npmlog: ^6.0.2 - nx: ">=14.6.1 < 16" - typescript: ^3 || ^4 + nx: ">=16.5.1 < 17" + p-map: 4.0.0 + p-map-series: 2.1.0 + p-pipe: 3.1.0 + p-queue: 6.6.2 + p-reduce: 2.1.0 + p-waterfall: 2.1.1 + pacote: ^15.2.0 + pify: 5.0.0 + read-cmd-shim: 4.0.0 + read-package-json: 6.0.4 + resolve-from: 5.0.0 + rimraf: ^4.4.1 + semver: ^7.3.8 + signal-exit: 3.0.7 + slash: 3.0.0 + ssri: ^9.0.1 + strong-log-transformer: 2.1.0 + tar: 6.1.11 + temp-dir: 1.0.0 + typescript: ">=3 < 6" + upath: 2.0.1 + uuid: ^9.0.0 + validate-npm-package-license: 3.0.4 + validate-npm-package-name: 5.0.0 + write-file-atomic: 5.0.1 + write-pkg: 4.0.0 + yargs: 16.2.0 + yargs-parser: 20.2.4 bin: - lerna: cli.js - checksum: 3107df46a5ce9d5bc4c5587767ac7b27d62b9732991853c48a58c88bdbc8ad972df7928e5cf98fbcb4d87ba5e47116cec5326cb9438f235e7a6686bc4e1c9ada + lerna: dist/cli.js + checksum: 5670bc36c9db19ae3bad13828e30dc0be53966bb86e78309477de5d1f866eef96f195cf9494a0de940e6403992189bbf7dbc21405b695c1a9a5dafe4aa5ae43c languageName: node linkType: hard @@ -21151,28 +20540,29 @@ __metadata: languageName: node linkType: hard -"libnpmaccess@npm:^6.0.3": - version: 6.0.4 - resolution: "libnpmaccess@npm:6.0.4" +"libnpmaccess@npm:7.0.2": + version: 7.0.2 + resolution: "libnpmaccess@npm:7.0.2" dependencies: - aproba: ^2.0.0 - minipass: ^3.1.1 - npm-package-arg: ^9.0.1 - npm-registry-fetch: ^13.0.0 - checksum: 86130b435c67a03254489c3b3684d435260b609164f76bcc69adbee78652c36a64551228b2c5ddc2b16851e9e367ee0ba173a641406768397716faa006042322 + npm-package-arg: ^10.1.0 + npm-registry-fetch: ^14.0.3 + checksum: 73d49f39391173276c46c12e32f503709338efd867d255d062ae9bc9e9f464d61240747f42bdd6dc6003a5dc275a27352ebfc11ed4cb424091463f302d823f23 languageName: node linkType: hard -"libnpmpublish@npm:^6.0.4": - version: 6.0.5 - resolution: "libnpmpublish@npm:6.0.5" +"libnpmpublish@npm:7.3.0": + version: 7.3.0 + resolution: "libnpmpublish@npm:7.3.0" dependencies: - normalize-package-data: ^4.0.0 - npm-package-arg: ^9.0.1 - npm-registry-fetch: ^13.0.0 + ci-info: ^3.6.1 + normalize-package-data: ^5.0.0 + npm-package-arg: ^10.1.0 + npm-registry-fetch: ^14.0.3 + proc-log: ^3.0.0 semver: ^7.3.7 - ssri: ^9.0.0 - checksum: d2f2434517038438be44db2e90e1c8c524df05f7c3b1458617177c2f9ca008dde8a72a4f739b34aee4df0352f71c9289788da86aa38a4709e05c6db33eed570a + sigstore: ^1.4.0 + ssri: ^10.0.1 + checksum: 03bedb65eb2293cfe5039f925ec1041deea698c5ac802bb74f6a0d44ee70529c38c32eea7c722f3a1f1219b54314021ad7f4764f93b66d619bea62ce0759faa0 languageName: node linkType: hard @@ -21190,7 +20580,7 @@ __metadata: languageName: node linkType: hard -"lines-and-columns@npm:^2.0.3": +"lines-and-columns@npm:^2.0.3, lines-and-columns@npm:~2.0.3": version: 2.0.3 resolution: "lines-and-columns@npm:2.0.3" checksum: 5955363dfd7d3d7c476d002eb47944dbe0310d57959e2112dce004c0dc76cecfd479cf8c098fd479ff344acdf04ee0e82b455462a26492231ac152f6c48d17a1 @@ -21218,6 +20608,18 @@ __metadata: languageName: node linkType: hard +"load-json-file@npm:6.2.0": + version: 6.2.0 + resolution: "load-json-file@npm:6.2.0" + dependencies: + graceful-fs: ^4.1.15 + parse-json: ^5.0.0 + strip-bom: ^4.0.0 + type-fest: ^0.6.0 + checksum: 4429e430ebb99375fc7cd936348e4f7ba729486080ced4272091c1e386a7f5f738ea3337d8ffd4b01c2f5bc3ddde92f2c780045b66838fe98bdb79f901884643 + languageName: node + linkType: hard + "load-json-file@npm:^4.0.0": version: 4.0.0 resolution: "load-json-file@npm:4.0.0" @@ -21230,18 +20632,6 @@ __metadata: languageName: node linkType: hard -"load-json-file@npm:^6.2.0": - version: 6.2.0 - resolution: "load-json-file@npm:6.2.0" - dependencies: - graceful-fs: ^4.1.15 - parse-json: ^5.0.0 - strip-bom: ^4.0.0 - type-fest: ^0.6.0 - checksum: 4429e430ebb99375fc7cd936348e4f7ba729486080ced4272091c1e386a7f5f738ea3337d8ffd4b01c2f5bc3ddde92f2c780045b66838fe98bdb79f901884643 - languageName: node - linkType: hard - "loader-runner@npm:^4.2.0": version: 4.2.0 resolution: "loader-runner@npm:4.2.0" @@ -21560,6 +20950,15 @@ __metadata: languageName: node linkType: hard +"make-dir@npm:4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: ^7.5.3 + checksum: bf0731a2dd3aab4db6f3de1585cea0b746bb73eb5a02e3d8d72757e376e64e6ada190b1eddcde5b2f24a81b688a9897efd5018737d05e02e2a671dda9cff8a8a + languageName: node + linkType: hard + "make-dir@npm:^2.0.0, make-dir@npm:^2.1.0": version: 2.1.0 resolution: "make-dir@npm:2.1.0" @@ -21586,7 +20985,7 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^10.0.3, make-fetch-happen@npm:^10.0.6": +"make-fetch-happen@npm:^10.0.3": version: 10.1.8 resolution: "make-fetch-happen@npm:10.1.8" dependencies: @@ -21610,6 +21009,29 @@ __metadata: languageName: node linkType: hard +"make-fetch-happen@npm:^11.0.0, make-fetch-happen@npm:^11.0.1, make-fetch-happen@npm:^11.1.1": + version: 11.1.1 + resolution: "make-fetch-happen@npm:11.1.1" + dependencies: + agentkeepalive: ^4.2.1 + cacache: ^17.0.0 + http-cache-semantics: ^4.1.1 + http-proxy-agent: ^5.0.0 + https-proxy-agent: ^5.0.0 + is-lambda: ^1.0.1 + lru-cache: ^7.7.1 + minipass: ^5.0.0 + minipass-fetch: ^3.0.0 + minipass-flush: ^1.0.5 + minipass-pipeline: ^1.2.4 + negotiator: ^0.6.3 + promise-retry: ^2.0.1 + socks-proxy-agent: ^7.0.0 + ssri: ^10.0.0 + checksum: 7268bf274a0f6dcf0343829489a4506603ff34bd0649c12058753900b0eb29191dce5dba12680719a5d0a983d3e57810f594a12f3c18494e93a1fbc6348a4540 + languageName: node + linkType: hard + "make-fetch-happen@npm:^9.1.0": version: 9.1.0 resolution: "make-fetch-happen@npm:9.1.0" @@ -21828,7 +21250,7 @@ __metadata: languageName: node linkType: hard -"meow@npm:^8.0.0": +"meow@npm:^8.1.2": version: 8.1.2 resolution: "meow@npm:8.1.2" dependencies: @@ -21998,12 +21420,21 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.1": - version: 9.0.1 - resolution: "minimatch@npm:9.0.1" +"minimatch@npm:^8.0.2": + version: 8.0.4 + resolution: "minimatch@npm:8.0.4" dependencies: brace-expansion: ^2.0.1 - checksum: 97f5f5284bb57dc65b9415dec7f17a0f6531a33572193991c60ff18450dcfad5c2dad24ffeaf60b5261dccd63aae58cc3306e2209d57e7f88c51295a532d8ec3 + checksum: 2e46cffb86bacbc524ad45a6426f338920c529dd13f3a732cc2cf7618988ee1aae88df4ca28983285aca9e0f45222019ac2d14ebd17c1edadd2ee12221ab801a + languageName: node + linkType: hard + +"minimatch@npm:^9.0.0, minimatch@npm:^9.0.1": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: ^2.0.1 + checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 languageName: node linkType: hard @@ -22064,6 +21495,21 @@ __metadata: languageName: node linkType: hard +"minipass-fetch@npm:^3.0.0": + version: 3.0.4 + resolution: "minipass-fetch@npm:3.0.4" + dependencies: + encoding: ^0.1.13 + minipass: ^7.0.3 + minipass-sized: ^1.0.3 + minizlib: ^2.1.2 + dependenciesMeta: + encoding: + optional: true + checksum: af7aad15d5c128ab1ebe52e043bdf7d62c3c6f0cecb9285b40d7b395e1375b45dcdfd40e63e93d26a0e8249c9efd5c325c65575aceee192883970ff8cb11364a + languageName: node + linkType: hard + "minipass-flush@npm:^1.0.5": version: 1.0.5 resolution: "minipass-flush@npm:1.0.5" @@ -22110,17 +21556,24 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^4.0.0": - version: 4.2.5 - resolution: "minipass@npm:4.2.5" - checksum: 4f9c19af23a5d4a9e7156feefc9110634b178a8cff8f8271af16ec5ebf7e221725a97429952c856f5b17b30c2065ebd24c81722d90c93d2122611d75b952b48f +"minipass@npm:^4.0.0, minipass@npm:^4.2.4": + version: 4.2.8 + resolution: "minipass@npm:4.2.8" + checksum: 7f4914d5295a9a30807cae5227a37a926e6d910c03f315930fde52332cf0575dfbc20295318f91f0baf0e6bb11a6f668e30cde8027dea7a11b9d159867a3c830 languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0": - version: 7.0.2 - resolution: "minipass@npm:7.0.2" - checksum: 46776de732eb7cef2c7404a15fb28c41f5c54a22be50d47b03c605bf21f5c18d61a173c0a20b49a97e7a65f78d887245066410642551e45fffe04e9ac9e325bc +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 425dab288738853fded43da3314a0b5c035844d6f3097a8e3b5b29b328da8f3c1af6fc70618b32c29ff906284cf6406b6841376f21caaadd0793c1d5a6a620ea + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.3": + version: 7.0.4 + resolution: "minipass@npm:7.0.4" + checksum: 87585e258b9488caf2e7acea242fd7856bbe9a2c84a7807643513a338d66f368c7d518200ad7b70a508664d408aa000517647b2930c259a8b1f9f0984f344a21 languageName: node linkType: hard @@ -22141,17 +21594,6 @@ __metadata: languageName: node linkType: hard -"mkdirp-infer-owner@npm:^2.0.0": - version: 2.0.0 - resolution: "mkdirp-infer-owner@npm:2.0.0" - dependencies: - chownr: ^2.0.0 - infer-owner: ^1.0.4 - mkdirp: ^1.0.3 - checksum: d8f4ecd32f6762459d6b5714eae6487c67ae9734ab14e26d14377ddd9b2a1bf868d8baa18c0f3e73d3d513f53ec7a698e0f81a9367102c870a55bef7833880f7 - languageName: node - linkType: hard - "mkdirp@npm:^0.5.1, mkdirp@npm:^0.5.4, mkdirp@npm:^0.5.6": version: 0.5.6 resolution: "mkdirp@npm:0.5.6" @@ -22220,7 +21662,7 @@ __metadata: languageName: node linkType: hard -"modify-values@npm:^1.0.0": +"modify-values@npm:^1.0.1": version: 1.0.1 resolution: "modify-values@npm:1.0.1" checksum: 8296610c608bc97b03c2cf889c6cdf4517e32fa2d836440096374c2209f6b7b3e256c209493a0b32584b9cb32d528e99d0dd19dcd9a14d2d915a312d391cc7e9 @@ -22375,7 +21817,7 @@ __metadata: languageName: node linkType: hard -"multimatch@npm:^5.0.0": +"multimatch@npm:5.0.0": version: 5.0.0 resolution: "multimatch@npm:5.0.0" dependencies: @@ -22402,13 +21844,20 @@ __metadata: languageName: node linkType: hard -"mute-stream@npm:0.0.8, mute-stream@npm:~0.0.4": +"mute-stream@npm:0.0.8": version: 0.0.8 resolution: "mute-stream@npm:0.0.8" checksum: ff48d251fc3f827e5b1206cda0ffdaec885e56057ee86a3155e1951bc940fd5f33531774b1cc8414d7668c10a8907f863f6561875ee6e8768931a62121a531a1 languageName: node linkType: hard +"mute-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "mute-stream@npm:1.0.0" + checksum: 36fc968b0e9c9c63029d4f9dc63911950a3bdf55c9a87f58d3a266289b67180201cade911e7699f8b2fa596b34c9db43dad37649e3f7fdd13c3bb9edb0017ee7 + languageName: node + linkType: hard + "nano-css@npm:^5.3.1": version: 5.3.4 resolution: "nano-css@npm:5.3.4" @@ -22526,7 +21975,21 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.0.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7": +"node-fetch@npm:2.6.7": + version: 2.6.7 + resolution: "node-fetch@npm:2.6.7" + dependencies: + whatwg-url: ^5.0.0 + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: 8d816ffd1ee22cab8301c7756ef04f3437f18dace86a1dae22cf81db8ef29c0bf6655f3215cb0cdb22b420b6fe141e64b26905e7f33f9377a7fa59135ea3e10b + languageName: node + linkType: hard + +"node-fetch@npm:^2.0.0, node-fetch@npm:^2.6.7": version: 2.6.12 resolution: "node-fetch@npm:2.6.12" dependencies: @@ -22605,6 +22068,13 @@ __metadata: languageName: node linkType: hard +"node-machine-id@npm:1.1.12": + version: 1.1.12 + resolution: "node-machine-id@npm:1.1.12" + checksum: e23088a0fb4a77a1d6484b7f09a22992fd3e0054d4f2e427692b4c7081e6cf30118ba07b6113b6c89f1ce46fd26ec5ab1d76dcaf6c10317717889124511283a5 + languageName: node + linkType: hard + "node-notifier@npm:10.0.1": version: 10.0.1 resolution: "node-notifier@npm:10.0.1" @@ -22649,7 +22119,7 @@ __metadata: languageName: node linkType: hard -"normalize-package-data@npm:^3.0.0, normalize-package-data@npm:^3.0.2": +"normalize-package-data@npm:^3.0.0, normalize-package-data@npm:^3.0.2, normalize-package-data@npm:^3.0.3": version: 3.0.3 resolution: "normalize-package-data@npm:3.0.3" dependencies: @@ -22661,15 +22131,15 @@ __metadata: languageName: node linkType: hard -"normalize-package-data@npm:^4.0.0": - version: 4.0.0 - resolution: "normalize-package-data@npm:4.0.0" +"normalize-package-data@npm:^5.0.0": + version: 5.0.0 + resolution: "normalize-package-data@npm:5.0.0" dependencies: - hosted-git-info: ^5.0.0 + hosted-git-info: ^6.0.0 is-core-module: ^2.8.1 semver: ^7.3.5 validate-npm-package-license: ^3.0.4 - checksum: b0f47de4295a0f8499bd478e84b9f9592a29f65227c2b4446ae80f7dff6e7a5ec6ef25ea8f06f3dcb9b7b7d945c2daa274385925b3d85e77e34eaffa0b42e316 + checksum: a459f05eaf7c2b643c61234177f08e28064fde97da15800e3d3ac0404e28450d43ac46fc95fbf6407a9bf20af4c58505ad73458a912dc1517f8c1687b1d68c27 languageName: node linkType: hard @@ -22705,7 +22175,7 @@ __metadata: languageName: node linkType: hard -"npm-bundled@npm:^1.1.1, npm-bundled@npm:^1.1.2": +"npm-bundled@npm:^1.1.2": version: 1.1.2 resolution: "npm-bundled@npm:1.1.2" dependencies: @@ -22714,26 +22184,35 @@ __metadata: languageName: node linkType: hard -"npm-install-checks@npm:^5.0.0": - version: 5.0.0 - resolution: "npm-install-checks@npm:5.0.0" +"npm-bundled@npm:^3.0.0": + version: 3.0.0 + resolution: "npm-bundled@npm:3.0.0" dependencies: - semver: ^7.1.1 - checksum: 0e7d1aae52b1fe9d3a0fd4a008850c7047931722dd49ee908afd13fd0297ac5ddb10964d9c59afcdaaa2ca04b51d75af2788f668c729ae71fec0e4cdac590ffc + npm-normalize-package-bin: ^3.0.0 + checksum: 110859c2d6dcd7941dac0932a29171cbde123060486a4b6e897aaf5e025abeb3d9ffcdfe9e9271992e6396b2986c2c534f1029a45a7c196f1257fa244305dbf8 languageName: node linkType: hard -"npm-normalize-package-bin@npm:^1.0.0, npm-normalize-package-bin@npm:^1.0.1": +"npm-install-checks@npm:^6.0.0": + version: 6.3.0 + resolution: "npm-install-checks@npm:6.3.0" + dependencies: + semver: ^7.1.1 + checksum: 6c20dadb878a0d2f1f777405217b6b63af1299d0b43e556af9363ee6eefaa98a17dfb7b612a473a473e96faf7e789c58b221e0d8ffdc1d34903c4f71618df3b4 + languageName: node + linkType: hard + +"npm-normalize-package-bin@npm:^1.0.1": version: 1.0.1 resolution: "npm-normalize-package-bin@npm:1.0.1" checksum: ae7f15155a1e3ace2653f12ddd1ee8eaa3c84452fdfbf2f1943e1de264e4b079c86645e2c55931a51a0a498cba31f70022a5219d5665fbcb221e99e58bc70122 languageName: node linkType: hard -"npm-normalize-package-bin@npm:^2.0.0": - version: 2.0.0 - resolution: "npm-normalize-package-bin@npm:2.0.0" - checksum: 7c5379f9b188b564c4332c97bdd9a5d6b7b15f02b5823b00989d6a0e6fb31eb0280f02b0a924f930e1fcaf00e60fae333aec8923d2a4c7747613c7d629d8aa25 +"npm-normalize-package-bin@npm:^3.0.0": + version: 3.0.1 + resolution: "npm-normalize-package-bin@npm:3.0.1" + checksum: de416d720ab22137a36292ff8a333af499ea0933ef2320a8c6f56a73b0f0448227fec4db5c890d702e26d21d04f271415eab6580b5546456861cc0c19498a4bf languageName: node linkType: hard @@ -22748,19 +22227,19 @@ __metadata: languageName: node linkType: hard -"npm-package-arg@npm:^9.0.0, npm-package-arg@npm:^9.0.1": - version: 9.1.0 - resolution: "npm-package-arg@npm:9.1.0" +"npm-package-arg@npm:^10.0.0, npm-package-arg@npm:^10.1.0": + version: 10.1.0 + resolution: "npm-package-arg@npm:10.1.0" dependencies: - hosted-git-info: ^5.0.0 - proc-log: ^2.0.1 + hosted-git-info: ^6.0.0 + proc-log: ^3.0.0 semver: ^7.3.5 - validate-npm-package-name: ^4.0.0 - checksum: 277c21477731a4f1e31bde36f0db5f5470deb2a008db2aaf1b015d588b23cb225c75f90291ea241235e86682a03de972bbe69fc805c921a786ea9616955990b9 + validate-npm-package-name: ^5.0.0 + checksum: 8fe4b6a742502345e4836ed42fdf26c544c9f75563c476c67044a481ada6e81f71b55462489c7e1899d516e4347150e58028036a90fa11d47e320bcc9365fd30 languageName: node linkType: hard -"npm-packlist@npm:^5.1.0, npm-packlist@npm:^5.1.1": +"npm-packlist@npm:5.1.1": version: 5.1.1 resolution: "npm-packlist@npm:5.1.1" dependencies: @@ -22774,30 +22253,39 @@ __metadata: languageName: node linkType: hard -"npm-pick-manifest@npm:^7.0.0": - version: 7.0.1 - resolution: "npm-pick-manifest@npm:7.0.1" +"npm-packlist@npm:^7.0.0": + version: 7.0.4 + resolution: "npm-packlist@npm:7.0.4" dependencies: - npm-install-checks: ^5.0.0 - npm-normalize-package-bin: ^1.0.1 - npm-package-arg: ^9.0.0 - semver: ^7.3.5 - checksum: 9a4a8e64d2214783b2b74a361845000f5d91bb40c7858e2a30af2ac7876d9296efc37f8cacf60335e96a45effee2035b033d9bdefb4889757cc60d85959accbb + ignore-walk: ^6.0.0 + checksum: 5ffa1f8f0b32141a60a66713fa3ed03b8ee4800b1ed6b59194d03c3c85da88f3fc21e1de29b665f322678bae85198732b16aa76c0a7cb0e283f9e0db50752233 languageName: node linkType: hard -"npm-registry-fetch@npm:^13.0.0, npm-registry-fetch@npm:^13.0.1, npm-registry-fetch@npm:^13.3.0": - version: 13.3.1 - resolution: "npm-registry-fetch@npm:13.3.1" +"npm-pick-manifest@npm:^8.0.0": + version: 8.0.2 + resolution: "npm-pick-manifest@npm:8.0.2" dependencies: - make-fetch-happen: ^10.0.6 - minipass: ^3.1.6 - minipass-fetch: ^2.0.3 + npm-install-checks: ^6.0.0 + npm-normalize-package-bin: ^3.0.0 + npm-package-arg: ^10.0.0 + semver: ^7.3.5 + checksum: c9f71b57351a3a241a7e56148332f2f341a09dff2a1b1f4ffb1517eac25f1888ac7fbce4939e522cbd533577448c307d05fff0c32430cc03c8c6179fac320cd4 + languageName: node + linkType: hard + +"npm-registry-fetch@npm:^14.0.0, npm-registry-fetch@npm:^14.0.3, npm-registry-fetch@npm:^14.0.5": + version: 14.0.5 + resolution: "npm-registry-fetch@npm:14.0.5" + dependencies: + make-fetch-happen: ^11.0.0 + minipass: ^5.0.0 + minipass-fetch: ^3.0.0 minipass-json-stream: ^1.0.1 minizlib: ^2.1.2 - npm-package-arg: ^9.0.1 - proc-log: ^2.0.0 - checksum: 5a941c2c799568e0dbccfc15f280444da398dadf2eede1b1921f08ddd5cb5f32c7cb4d16be96401f95a33073aeec13a3fd928c753790d3c412c2e64e7f7c6ee4 + npm-package-arg: ^10.0.0 + proc-log: ^3.0.0 + checksum: c63649642955b424bc1baaff5955027144af312ae117ba8c24829e74484f859482591fe89687c6597d83e930c8054463eef23020ac69146097a72cc62ff10986 languageName: node linkType: hard @@ -22857,47 +22345,80 @@ __metadata: languageName: node linkType: hard -"nx@npm:14.8.2, nx@npm:>=14.6.1 < 16": - version: 14.8.2 - resolution: "nx@npm:14.8.2" +"nx@npm:16.10.0, nx@npm:>=16.5.1 < 17": + version: 16.10.0 + resolution: "nx@npm:16.10.0" dependencies: - "@nrwl/cli": 14.8.2 - "@nrwl/tao": 14.8.2 + "@nrwl/tao": 16.10.0 + "@nx/nx-darwin-arm64": 16.10.0 + "@nx/nx-darwin-x64": 16.10.0 + "@nx/nx-freebsd-x64": 16.10.0 + "@nx/nx-linux-arm-gnueabihf": 16.10.0 + "@nx/nx-linux-arm64-gnu": 16.10.0 + "@nx/nx-linux-arm64-musl": 16.10.0 + "@nx/nx-linux-x64-gnu": 16.10.0 + "@nx/nx-linux-x64-musl": 16.10.0 + "@nx/nx-win32-arm64-msvc": 16.10.0 + "@nx/nx-win32-x64-msvc": 16.10.0 "@parcel/watcher": 2.0.4 "@yarnpkg/lockfile": ^1.1.0 - "@yarnpkg/parsers": ^3.0.0-rc.18 + "@yarnpkg/parsers": 3.0.0-rc.46 "@zkochan/js-yaml": 0.0.6 - chalk: 4.1.0 - chokidar: ^3.5.1 + axios: ^1.0.0 + chalk: ^4.1.0 cli-cursor: 3.1.0 cli-spinners: 2.6.1 - cliui: ^7.0.2 - dotenv: ~10.0.0 + cliui: ^8.0.1 + dotenv: ~16.3.1 + dotenv-expand: ~10.0.0 enquirer: ~2.3.6 - fast-glob: 3.2.7 figures: 3.2.0 flat: ^5.0.2 - fs-extra: ^10.1.0 + fs-extra: ^11.1.0 glob: 7.1.4 ignore: ^5.0.4 + jest-diff: ^29.4.1 js-yaml: 4.1.0 jsonc-parser: 3.2.0 + lines-and-columns: ~2.0.3 minimatch: 3.0.5 + node-machine-id: 1.1.12 npm-run-path: ^4.0.1 open: ^8.4.0 - semver: 7.3.4 + semver: 7.5.3 string-width: ^4.2.3 strong-log-transformer: ^2.1.0 tar-stream: ~2.2.0 tmp: ~0.2.1 - tsconfig-paths: ^3.9.0 + tsconfig-paths: ^4.1.2 tslib: ^2.3.0 v8-compile-cache: 2.3.0 - yargs: ^17.4.0 - yargs-parser: 21.0.1 + yargs: ^17.6.2 + yargs-parser: 21.1.1 peerDependencies: - "@swc-node/register": ^1.4.2 - "@swc/core": ^1.2.173 + "@swc-node/register": ^1.6.7 + "@swc/core": ^1.3.85 + dependenciesMeta: + "@nx/nx-darwin-arm64": + optional: true + "@nx/nx-darwin-x64": + optional: true + "@nx/nx-freebsd-x64": + optional: true + "@nx/nx-linux-arm-gnueabihf": + optional: true + "@nx/nx-linux-arm64-gnu": + optional: true + "@nx/nx-linux-arm64-musl": + optional: true + "@nx/nx-linux-x64-gnu": + optional: true + "@nx/nx-linux-x64-musl": + optional: true + "@nx/nx-win32-arm64-msvc": + optional: true + "@nx/nx-win32-x64-msvc": + optional: true peerDependenciesMeta: "@swc-node/register": optional: true @@ -22905,7 +22426,7 @@ __metadata: optional: true bin: nx: bin/nx.js - checksum: b0c0428366f867e20d5f89d8e9bf2f8c8b6f9c0a60a7b8bebc3617d652b0e33109bc8bce352b9e7218db69eb181b0bacb3378c3d0f5b063acfd986ed0b35f7df + checksum: 961b290f65dba76cf6cda62377930ac70fb5546d2992fde19ab028c7b4c37b76fc14eaa89f1d071b95e6d701932a2fd77678849172115045fd835ef9758e93bb languageName: node linkType: hard @@ -23280,14 +22801,14 @@ __metadata: languageName: node linkType: hard -"p-map-series@npm:^2.1.0": +"p-map-series@npm:2.1.0": version: 2.1.0 resolution: "p-map-series@npm:2.1.0" checksum: 69d4efbb6951c0dd62591d5a18c3af0af78496eae8b55791e049da239d70011aa3af727dece3fc9943e0bb3fd4fa64d24177cfbecc46efaf193179f0feeac486 languageName: node linkType: hard -"p-map@npm:^4.0.0": +"p-map@npm:4.0.0, p-map@npm:^4.0.0": version: 4.0.0 resolution: "p-map@npm:4.0.0" dependencies: @@ -23296,14 +22817,14 @@ __metadata: languageName: node linkType: hard -"p-pipe@npm:^3.1.0": +"p-pipe@npm:3.1.0": version: 3.1.0 resolution: "p-pipe@npm:3.1.0" checksum: ee9a2609685f742c6ceb3122281ec4453bbbcc80179b13e66fd139dcf19b1c327cf6c2fdfc815b548d6667e7eaefe5396323f6d49c4f7933e4cef47939e3d65c languageName: node linkType: hard -"p-queue@npm:^6.6.2": +"p-queue@npm:6.6.2": version: 6.6.2 resolution: "p-queue@npm:6.6.2" dependencies: @@ -23313,7 +22834,7 @@ __metadata: languageName: node linkType: hard -"p-reduce@npm:^2.0.0, p-reduce@npm:^2.1.0": +"p-reduce@npm:2.1.0, p-reduce@npm:^2.0.0, p-reduce@npm:^2.1.0": version: 2.1.0 resolution: "p-reduce@npm:2.1.0" checksum: 99b26d36066a921982f25c575e78355824da0787c486e3dd9fc867460e8bf17d5fb3ce98d006b41bdc81ffc0aa99edf5faee53d11fe282a20291fb721b0cb1c7 @@ -23353,7 +22874,7 @@ __metadata: languageName: node linkType: hard -"p-waterfall@npm:^2.1.1": +"p-waterfall@npm:2.1.1": version: 2.1.1 resolution: "p-waterfall@npm:2.1.1" dependencies: @@ -23362,34 +22883,31 @@ __metadata: languageName: node linkType: hard -"pacote@npm:^13.0.3, pacote@npm:^13.6.1": - version: 13.6.1 - resolution: "pacote@npm:13.6.1" +"pacote@npm:^15.2.0": + version: 15.2.0 + resolution: "pacote@npm:15.2.0" dependencies: - "@npmcli/git": ^3.0.0 - "@npmcli/installed-package-contents": ^1.0.7 - "@npmcli/promise-spawn": ^3.0.0 - "@npmcli/run-script": ^4.1.0 - cacache: ^16.0.0 - chownr: ^2.0.0 - fs-minipass: ^2.1.0 - infer-owner: ^1.0.4 - minipass: ^3.1.6 - mkdirp: ^1.0.4 - npm-package-arg: ^9.0.0 - npm-packlist: ^5.1.0 - npm-pick-manifest: ^7.0.0 - npm-registry-fetch: ^13.0.1 - proc-log: ^2.0.0 + "@npmcli/git": ^4.0.0 + "@npmcli/installed-package-contents": ^2.0.1 + "@npmcli/promise-spawn": ^6.0.1 + "@npmcli/run-script": ^6.0.0 + cacache: ^17.0.0 + fs-minipass: ^3.0.0 + minipass: ^5.0.0 + npm-package-arg: ^10.0.0 + npm-packlist: ^7.0.0 + npm-pick-manifest: ^8.0.0 + npm-registry-fetch: ^14.0.0 + proc-log: ^3.0.0 promise-retry: ^2.0.1 - read-package-json: ^5.0.0 - read-package-json-fast: ^2.0.3 - rimraf: ^3.0.2 - ssri: ^9.0.0 + read-package-json: ^6.0.0 + read-package-json-fast: ^3.0.0 + sigstore: ^1.3.0 + ssri: ^10.0.0 tar: ^6.1.11 bin: pacote: lib/bin.js - checksum: 26cebb59aea93d03ad051d82c4f2300beb333ded0f16ba92cfe976b5600157bd1ee034afe1c86406bbe5eacd51d413797939b08aa58adcf73f7680aead9e667f + checksum: c731572be2bf226b117eba076d242bd4cd8be7aa01e004af3374a304ad7ab330539e22644bc33de12d2a7d45228ccbcbf4d710f59c84414f3d09a1a95ee6f0bf languageName: node linkType: hard @@ -23433,17 +22951,6 @@ __metadata: languageName: node linkType: hard -"parse-conflict-json@npm:^2.0.1": - version: 2.0.2 - resolution: "parse-conflict-json@npm:2.0.2" - dependencies: - json-parse-even-better-errors: ^2.3.1 - just-diff: ^5.0.1 - just-diff-apply: ^5.2.0 - checksum: 076f65c958696586daefb153f59d575dfb59648be43116a21b74d5ff69ec63dd56f585a27cc2da56d8e64ca5abf0373d6619b8330c035131f8d1e990c8406378 - languageName: node - linkType: hard - "parse-entities@npm:^2.0.0": version: 2.0.0 resolution: "parse-entities@npm:2.0.0" @@ -23604,7 +23111,7 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^1.10.1": +"path-scurry@npm:^1.10.1, path-scurry@npm:^1.6.1": version: 1.10.1 resolution: "path-scurry@npm:1.10.1" dependencies: @@ -23711,6 +23218,13 @@ __metadata: languageName: node linkType: hard +"pify@npm:5.0.0": + version: 5.0.0 + resolution: "pify@npm:5.0.0" + checksum: 443e3e198ad6bfa8c0c533764cf75c9d5bc976387a163792fb553ffe6ce923887cf14eebf5aea9b7caa8eab930da8c33612990ae85bd8c2bc18bedb9eae94ecb + languageName: node + linkType: hard + "pify@npm:^2.2.0, pify@npm:^2.3.0": version: 2.3.0 resolution: "pify@npm:2.3.0" @@ -23732,13 +23246,6 @@ __metadata: languageName: node linkType: hard -"pify@npm:^5.0.0": - version: 5.0.0 - resolution: "pify@npm:5.0.0" - checksum: 443e3e198ad6bfa8c0c533764cf75c9d5bc976387a163792fb553ffe6ce923887cf14eebf5aea9b7caa8eab930da8c33612990ae85bd8c2bc18bedb9eae94ecb - languageName: node - linkType: hard - "pirates@npm:^4.0.4, pirates@npm:^4.0.5": version: 4.0.5 resolution: "pirates@npm:4.0.5" @@ -24378,10 +23885,10 @@ __metadata: languageName: node linkType: hard -"proc-log@npm:^2.0.0, proc-log@npm:^2.0.1": - version: 2.0.1 - resolution: "proc-log@npm:2.0.1" - checksum: f6f23564ff759097db37443e6e2765af84979a703d2c52c1b9df506ee9f87caa101ba49d8fdc115c1a313ec78e37e8134704e9069e6a870f3499d98bb24c436f +"proc-log@npm:^3.0.0": + version: 3.0.0 + resolution: "proc-log@npm:3.0.0" + checksum: 02b64e1b3919e63df06f836b98d3af002b5cd92655cab18b5746e37374bfb73e03b84fe305454614b34c25b485cc687a9eebdccf0242cda8fda2475dd2c97e02 languageName: node linkType: hard @@ -24406,20 +23913,6 @@ __metadata: languageName: node linkType: hard -"promise-all-reject-late@npm:^1.0.0": - version: 1.0.1 - resolution: "promise-all-reject-late@npm:1.0.1" - checksum: d7d61ac412352e2c8c3463caa5b1c3ca0f0cc3db15a09f180a3da1446e33d544c4261fc716f772b95e4c27d559cfd2388540f44104feb356584f9c73cfb9ffcb - languageName: node - linkType: hard - -"promise-call-limit@npm:^1.0.1": - version: 1.0.1 - resolution: "promise-call-limit@npm:1.0.1" - checksum: e69aed17f5f34bbd7aecff28faedb456e3500a08af31ee759ef75f2d8c2219d7c0e59f153f4d8c339056de8c304e0dd4acc500c339e7ea1e9c0e7bb1444367c8 - languageName: node - linkType: hard - "promise-inflight@npm:^1.0.1": version: 1.0.1 resolution: "promise-inflight@npm:1.0.1" @@ -24463,12 +23956,12 @@ __metadata: languageName: node linkType: hard -"promzard@npm:^0.3.0": - version: 0.3.0 - resolution: "promzard@npm:0.3.0" +"promzard@npm:^1.0.0": + version: 1.0.0 + resolution: "promzard@npm:1.0.0" dependencies: - read: 1 - checksum: 443a3b39ac916099988ee0161ab4e22edd1fa27e3d39a38d60e48c11ca6df3f5a90bfe44d95af06ed8659c4050b789ffe64c3f9f8e49a4bea1ea19105c98445a + read: ^2.0.0 + checksum: c06948827171612faae321ebaf23ff8bd9ebb3e1e0f37616990bc4b81c663b192e447b3fe3b424211beb0062cec0cfe6ba3ce70c8b448b4aa59752b765dbb302 languageName: node linkType: hard @@ -24501,13 +23994,6 @@ __metadata: languageName: node linkType: hard -"proto-list@npm:~1.2.1": - version: 1.2.4 - resolution: "proto-list@npm:1.2.4" - checksum: 4d4826e1713cbfa0f15124ab0ae494c91b597a3c458670c9714c36e8baddf5a6aad22842776f2f5b137f259c8533e741771445eb8df82e861eea37a6eaba03f7 - languageName: node - linkType: hard - "protobufjs@npm:^7.2.4": version: 7.2.4 resolution: "protobufjs@npm:7.2.4" @@ -24559,7 +24045,7 @@ __metadata: languageName: node linkType: hard -"proxy-from-env@npm:^1.0.0": +"proxy-from-env@npm:^1.0.0, proxy-from-env@npm:^1.1.0": version: 1.1.0 resolution: "proxy-from-env@npm:1.1.0" checksum: ed7fcc2ba0a33404958e34d95d18638249a68c430e30fcb6c478497d72739ba64ce9810a24f53a7d921d0c065e5b78e3822759800698167256b04659366ca4d4 @@ -24657,13 +24143,6 @@ __metadata: languageName: node linkType: hard -"q@npm:^1.5.1": - version: 1.5.1 - resolution: "q@npm:1.5.1" - checksum: 147baa93c805bc1200ed698bdf9c72e9e42c05f96d007e33a558b5fdfd63e5ea130e99313f28efc1783e90e6bdb4e48b67a36fcc026b7b09202437ae88a1fb12 - languageName: node - linkType: hard - "qs@npm:6.10.4, qs@npm:~6.10.3": version: 6.10.4 resolution: "qs@npm:6.10.4" @@ -26000,32 +25479,32 @@ __metadata: languageName: node linkType: hard -"read-cmd-shim@npm:^3.0.0": - version: 3.0.0 - resolution: "read-cmd-shim@npm:3.0.0" - checksum: b518c6026f3320e30b692044f6ff5c4dc80f9c71261296da8994101b569b26b12b8e5df397bba2d4691dd3a3a2f770a1eca7be18a69ec202fac6dcfadc5016fd +"read-cmd-shim@npm:4.0.0": + version: 4.0.0 + resolution: "read-cmd-shim@npm:4.0.0" + checksum: 2fb5a8a38984088476f559b17c6a73324a5db4e77e210ae0aab6270480fd85c355fc990d1c79102e25e555a8201606ed12844d6e3cd9f35d6a1518791184e05b languageName: node linkType: hard -"read-package-json-fast@npm:^2.0.2, read-package-json-fast@npm:^2.0.3": - version: 2.0.3 - resolution: "read-package-json-fast@npm:2.0.3" +"read-package-json-fast@npm:^3.0.0": + version: 3.0.2 + resolution: "read-package-json-fast@npm:3.0.2" dependencies: - json-parse-even-better-errors: ^2.3.0 - npm-normalize-package-bin: ^1.0.1 - checksum: fca37b3b2160b9dda7c5588b767f6a2b8ce68d03a044000e568208e20bea0cf6dd2de17b90740ce8da8b42ea79c0b3859649dadf29510bbe77224ea65326a903 + json-parse-even-better-errors: ^3.0.0 + npm-normalize-package-bin: ^3.0.0 + checksum: 8d406869f045f1d76e2a99865a8fd1c1af9c1dc06200b94d2b07eef87ed734b22703a8d72e1cd36ea36cc48e22020bdd187f88243c7dd0563f72114d38c17072 languageName: node linkType: hard -"read-package-json@npm:^5.0.0, read-package-json@npm:^5.0.1": - version: 5.0.2 - resolution: "read-package-json@npm:5.0.2" +"read-package-json@npm:6.0.4, read-package-json@npm:^6.0.0": + version: 6.0.4 + resolution: "read-package-json@npm:6.0.4" dependencies: - glob: ^8.0.1 - json-parse-even-better-errors: ^2.3.1 - normalize-package-data: ^4.0.0 - npm-normalize-package-bin: ^2.0.0 - checksum: 0882ac9cec1bc92fb5515e9727611fb2909351e1e5c840dce3503cbb25b4cd48eb44b61071986e0fc51043208161f07d364a7336206c8609770186818753b51a + glob: ^10.2.2 + json-parse-even-better-errors: ^3.0.0 + normalize-package-data: ^5.0.0 + npm-normalize-package-bin: ^3.0.0 + checksum: ce40c4671299753f1349aebe44693cd250d6936c4bacfb31cd884c87f24a0174ba5f651ee2866cf5e57365451cba38bc1db9c2a371e4ba7502fb46dcad50f1d7 languageName: node linkType: hard @@ -26096,12 +25575,12 @@ __metadata: languageName: node linkType: hard -"read@npm:1, read@npm:^1.0.7": - version: 1.0.7 - resolution: "read@npm:1.0.7" +"read@npm:^2.0.0": + version: 2.1.0 + resolution: "read@npm:2.1.0" dependencies: - mute-stream: ~0.0.4 - checksum: 2777c254e5732cac96f5d0a1c0f6b836c89ae23d8febd405b206f6f24d5de1873420f1a0795e0e3721066650d19adf802c7882c4027143ee0acf942a4f34f97b + mute-stream: ~1.0.0 + checksum: e745999138022b56d32daf7cce9b7552b2ec648e4e2578d076a410575a0a400faf74f633dd74ef1b1c42563397d322c1ad5a0068471c38978b02ef97056c2991 languageName: node linkType: hard @@ -26143,18 +25622,6 @@ __metadata: languageName: node linkType: hard -"readdir-scoped-modules@npm:^1.1.0": - version: 1.1.0 - resolution: "readdir-scoped-modules@npm:1.1.0" - dependencies: - debuglog: ^1.0.1 - dezalgo: ^1.0.0 - graceful-fs: ^4.1.2 - once: ^1.3.0 - checksum: 6d9f334e40dfd0f5e4a8aab5e67eb460c95c85083c690431f87ab2c9135191170e70c2db6d71afcafb78e073d23eb95dcb3fc33ef91308f6ebfe3197be35e608 - languageName: node - linkType: hard - "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -26542,6 +26009,13 @@ __metadata: languageName: node linkType: hard +"resolve-from@npm:5.0.0, resolve-from@npm:^5.0.0": + version: 5.0.0 + resolution: "resolve-from@npm:5.0.0" + checksum: 4ceeb9113e1b1372d0cd969f3468fa042daa1dd9527b1b6bb88acb6ab55d8b9cd65dbf18819f9f9ddf0db804990901dcdaade80a215e7b2c23daae38e64f5bdf + languageName: node + linkType: hard + "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -26549,13 +26023,6 @@ __metadata: languageName: node linkType: hard -"resolve-from@npm:^5.0.0": - version: 5.0.0 - resolution: "resolve-from@npm:5.0.0" - checksum: 4ceeb9113e1b1372d0cd969f3468fa042daa1dd9527b1b6bb88acb6ab55d8b9cd65dbf18819f9f9ddf0db804990901dcdaade80a215e7b2c23daae38e64f5bdf - languageName: node - linkType: hard - "resolve-options@npm:^1.1.0": version: 1.1.0 resolution: "resolve-options@npm:1.1.0" @@ -26725,6 +26192,17 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:^4.4.1": + version: 4.4.1 + resolution: "rimraf@npm:4.4.1" + dependencies: + glob: ^9.2.0 + bin: + rimraf: dist/cjs/src/bin.js + checksum: b786adc02651e2e24bbedb04bbdea80652fc9612632931ff2d9f898c5e4708fe30956186597373c568bd5230a4dc2fadfc816ccacba8a1daded3a006a6b74f1a + languageName: node + linkType: hard + "rimraf@npm:~2.6.2": version: 2.6.3 resolution: "rimraf@npm:2.6.3" @@ -27096,6 +26574,17 @@ __metadata: languageName: node linkType: hard +"semver@npm:7.5.3": + version: 7.5.3 + resolution: "semver@npm:7.5.3" + dependencies: + lru-cache: ^6.0.0 + bin: + semver: bin/semver.js + checksum: 9d58db16525e9f749ad0a696a1f27deabaa51f66e91d2fa2b0db3de3e9644e8677de3b7d7a03f4c15bc81521e0c3916d7369e0572dbde250d9bedf5194e2a8a7 + languageName: node + linkType: hard + "semver@npm:7.5.4, semver@npm:7.x, 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.1, semver@npm:^7.5.3, semver@npm:^7.5.4": version: 7.5.4 resolution: "semver@npm:7.5.4" @@ -27307,7 +26796,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": +"signal-exit@npm:3.0.7, signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 @@ -27321,6 +26810,21 @@ __metadata: languageName: node linkType: hard +"sigstore@npm:^1.3.0, sigstore@npm:^1.4.0": + version: 1.9.0 + resolution: "sigstore@npm:1.9.0" + dependencies: + "@sigstore/bundle": ^1.1.0 + "@sigstore/protobuf-specs": ^0.2.0 + "@sigstore/sign": ^1.0.0 + "@sigstore/tuf": ^1.0.3 + make-fetch-happen: ^11.0.1 + bin: + sigstore: bin/sigstore.js + checksum: b3f1ccf4d2d5e6af294ad851981cc9dc4c01b6b5b7aeb98582765f5d2e75aa2b9221133b8e572179bb305e16ce589339d9617b26b9fa0bea0c38c9adef792912 + languageName: node + linkType: hard + "simple-git@npm:^3.6.0": version: 3.16.0 resolution: "simple-git@npm:3.16.0" @@ -27366,7 +26870,7 @@ __metadata: languageName: node linkType: hard -"slash@npm:^3.0.0": +"slash@npm:3.0.0, slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" checksum: 94a93fff615f25a999ad4b83c9d5e257a7280c90a32a7cb8b4a87996e4babf322e469c42b7f649fd5796edd8687652f3fb452a86dc97a816f01113183393f11c @@ -27592,15 +27096,6 @@ __metadata: languageName: node linkType: hard -"sort-keys@npm:^4.0.0": - version: 4.2.0 - resolution: "sort-keys@npm:4.2.0" - dependencies: - is-plain-obj: ^2.0.0 - checksum: 1535ffd5a789259fc55107d5c3cec09b3e47803a9407fcaae37e1b9e0b813762c47dfee35b6e71e20ca7a69798d0a4791b2058a07f6cab5ef17b2dae83cedbda - languageName: node - linkType: hard - "sort-keys@npm:^5.0.0": version: 5.0.0 resolution: "sort-keys@npm:5.0.0" @@ -27774,7 +27269,7 @@ __metadata: languageName: node linkType: hard -"split2@npm:^3.0.0": +"split2@npm:^3.2.2": version: 3.2.2 resolution: "split2@npm:3.2.2" dependencies: @@ -27792,7 +27287,7 @@ __metadata: languageName: node linkType: hard -"split@npm:^1.0.0": +"split@npm:^1.0.1": version: 1.0.1 resolution: "split@npm:1.0.1" dependencies: @@ -27853,6 +27348,15 @@ __metadata: languageName: node linkType: hard +"ssri@npm:^10.0.0, ssri@npm:^10.0.1": + version: 10.0.5 + resolution: "ssri@npm:10.0.5" + dependencies: + minipass: ^7.0.3 + checksum: 0a31b65f21872dea1ed3f7c200d7bc1c1b91c15e419deca14f282508ba917cbb342c08a6814c7f68ca4ca4116dd1a85da2bbf39227480e50125a1ceffeecb750 + languageName: node + linkType: hard + "ssri@npm:^8.0.0, ssri@npm:^8.0.1": version: 8.0.1 resolution: "ssri@npm:8.0.1" @@ -28248,7 +27752,7 @@ __metadata: languageName: node linkType: hard -"strong-log-transformer@npm:^2.1.0": +"strong-log-transformer@npm:2.1.0, strong-log-transformer@npm:^2.1.0": version: 2.1.0 resolution: "strong-log-transformer@npm:2.1.0" dependencies: @@ -28563,7 +28067,21 @@ __metadata: languageName: node linkType: hard -"tar@npm:^6.0.2, tar@npm:^6.1.0, tar@npm:^6.1.11, tar@npm:^6.1.13, tar@npm:^6.1.2": +"tar@npm:6.1.11": + version: 6.1.11 + resolution: "tar@npm:6.1.11" + dependencies: + chownr: ^2.0.0 + fs-minipass: ^2.0.0 + minipass: ^3.0.0 + minizlib: ^2.1.1 + mkdirp: ^1.0.3 + yallist: ^4.0.0 + checksum: a04c07bb9e2d8f46776517d4618f2406fb977a74d914ad98b264fc3db0fe8224da5bec11e5f8902c5b9bcb8ace22d95fbe3c7b36b8593b7dfc8391a25898f32f + languageName: node + linkType: hard + +"tar@npm:^6.0.2, tar@npm:^6.1.11, tar@npm:^6.1.13, tar@npm:^6.1.2": version: 6.1.13 resolution: "tar@npm:6.1.13" dependencies: @@ -28595,7 +28113,7 @@ __metadata: languageName: node linkType: hard -"temp-dir@npm:^1.0.0": +"temp-dir@npm:1.0.0": version: 1.0.0 resolution: "temp-dir@npm:1.0.0" checksum: cb2b58ddfb12efa83e939091386ad73b425c9a8487ea0095fe4653192a40d49184a771a1beba99045fbd011e389fd563122d79f54f82be86a55620667e08a6b2 @@ -28773,7 +28291,7 @@ __metadata: languageName: node linkType: hard -"through2@npm:^4.0.0, through2@npm:~4.0.2": +"through2@npm:~4.0.2": version: 4.0.2 resolution: "through2@npm:4.0.2" dependencies: @@ -28989,13 +28507,6 @@ __metadata: languageName: node linkType: hard -"treeverse@npm:^2.0.0": - version: 2.0.0 - resolution: "treeverse@npm:2.0.0" - checksum: 3c6b2b890975a4d42c86b9a0f1eb932b4450db3fa874be5c301c4f5e306fd76330c6a490cf334b0937b3a44b049787ba5d98c88bc7b140f34fdb3ab1f83e5269 - languageName: node - linkType: hard - "trim-newlines@npm:^3.0.0": version: 3.0.1 resolution: "trim-newlines@npm:3.0.1" @@ -29153,7 +28664,7 @@ __metadata: languageName: node linkType: hard -"tsconfig-paths@npm:^3.14.2, tsconfig-paths@npm:^3.9.0": +"tsconfig-paths@npm:^3.14.2": version: 3.14.2 resolution: "tsconfig-paths@npm:3.14.2" dependencies: @@ -29165,6 +28676,17 @@ __metadata: languageName: node linkType: hard +"tsconfig-paths@npm:^4.1.2": + version: 4.2.0 + resolution: "tsconfig-paths@npm:4.2.0" + dependencies: + json5: ^2.2.2 + minimist: ^1.2.6 + strip-bom: ^3.0.0 + checksum: 28c5f7bbbcabc9dabd4117e8fdc61483f6872a1c6b02a4b1c4d68c5b79d06896c3cc9547610c4c3ba64658531caa2de13ead1ea1bf321c7b53e969c4752b98c7 + languageName: node + linkType: hard + "tslib@npm:2.4.0": version: 2.4.0 resolution: "tslib@npm:2.4.0" @@ -29204,6 +28726,17 @@ __metadata: languageName: node linkType: hard +"tuf-js@npm:^1.1.7": + version: 1.1.7 + resolution: "tuf-js@npm:1.1.7" + dependencies: + "@tufjs/models": 1.0.4 + debug: ^4.3.4 + make-fetch-happen: ^11.1.1 + checksum: 089fc0dabe1fcaeca8b955b358b34272f23237ac9e074b5f983349eb44d9688fd137f28f493bbd8dfd865d1af4e76e0cc869d307eadd054d1b404914c3124ae5 + languageName: node + linkType: hard + "tunnel-agent@npm:^0.6.0": version: 0.6.0 resolution: "tunnel-agent@npm:0.6.0" @@ -29370,15 +28903,6 @@ __metadata: languageName: node linkType: hard -"typedarray-to-buffer@npm:^3.1.5": - version: 3.1.5 - resolution: "typedarray-to-buffer@npm:3.1.5" - dependencies: - is-typedarray: ^1.0.0 - checksum: 99c11aaa8f45189fcfba6b8a4825fd684a321caa9bd7a76a27cf0c7732c174d198b99f449c52c3818107430b5f41c0ccbbfb75cb2ee3ca4a9451710986d61a60 - languageName: node - linkType: hard - "typedarray@npm:^0.0.6": version: 0.0.6 resolution: "typedarray@npm:0.0.6" @@ -29386,7 +28910,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:4.8.4, typescript@npm:>=2.7, typescript@npm:^3 || ^4, typescript@npm:^4.2.4": +"typescript@npm:4.8.4, typescript@npm:^4.2.4": version: 4.8.4 resolution: "typescript@npm:4.8.4" bin: @@ -29396,7 +28920,17 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@4.8.4#~builtin, typescript@patch:typescript@>=2.7#~builtin, typescript@patch:typescript@^3 || ^4#~builtin, typescript@patch:typescript@^4.2.4#~builtin": +"typescript@npm:>=2.7, typescript@npm:>=3 < 6": + version: 5.2.2 + resolution: "typescript@npm:5.2.2" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 7912821dac4d962d315c36800fe387cdc0a6298dba7ec171b350b4a6e988b51d7b8f051317786db1094bd7431d526b648aba7da8236607febb26cf5b871d2d3c + languageName: node + linkType: hard + +"typescript@patch:typescript@4.8.4#~builtin, typescript@patch:typescript@^4.2.4#~builtin": version: 4.8.4 resolution: "typescript@patch:typescript@npm%3A4.8.4#~builtin::version=4.8.4&hash=1a91c8" bin: @@ -29406,6 +28940,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@>=2.7#~builtin, typescript@patch:typescript@>=3 < 6#~builtin": + version: 5.2.2 + resolution: "typescript@patch:typescript@npm%3A5.2.2#~builtin::version=5.2.2&hash=f3b441" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 0f4da2f15e6f1245e49db15801dbee52f2bbfb267e1c39225afdab5afee1a72839cd86000e65ee9d7e4dfaff12239d28beaf5ee431357fcced15fb08583d72ca + languageName: node + linkType: hard + "ua-parser-js@npm:^1.0.32": version: 1.0.33 resolution: "ua-parser-js@npm:1.0.33" @@ -29507,6 +29051,15 @@ __metadata: languageName: node linkType: hard +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" + dependencies: + unique-slug: ^4.0.0 + checksum: 8e2f59b356cb2e54aab14ff98a51ac6c45781d15ceaab6d4f1c2228b780193dc70fae4463ce9e1df4479cb9d3304d7c2043a3fb905bdeca71cc7e8ce27e063df + languageName: node + linkType: hard + "unique-slug@npm:^2.0.0": version: 2.0.2 resolution: "unique-slug@npm:2.0.2" @@ -29516,6 +29069,15 @@ __metadata: languageName: node linkType: hard +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" + dependencies: + imurmurhash: ^0.1.4 + checksum: 0884b58365af59f89739e6f71e3feacb5b1b41f2df2d842d0757933620e6de08eff347d27e9d499b43c40476cbaf7988638d3acb2ffbcb9d35fd035591adfd15 + languageName: node + linkType: hard + "unique-stream@npm:^2.0.2": version: 2.3.1 resolution: "unique-stream@npm:2.3.1" @@ -29617,7 +29179,7 @@ __metadata: languageName: node linkType: hard -"upath@npm:^2.0.1": +"upath@npm:2.0.1": version: 2.0.1 resolution: "upath@npm:2.0.1" checksum: 2db04f24a03ef72204c7b969d6991abec9e2cb06fb4c13a1fd1c59bc33b46526b16c3325e55930a11ff86a77a8cbbcda8f6399bf914087028c5beae21ecdb33c @@ -29849,7 +29411,7 @@ __metadata: languageName: node linkType: hard -"validate-npm-package-license@npm:^3.0.1, validate-npm-package-license@npm:^3.0.4": +"validate-npm-package-license@npm:3.0.4, validate-npm-package-license@npm:^3.0.1, validate-npm-package-license@npm:^3.0.4": version: 3.0.4 resolution: "validate-npm-package-license@npm:3.0.4" dependencies: @@ -29859,6 +29421,15 @@ __metadata: languageName: node linkType: hard +"validate-npm-package-name@npm:5.0.0, validate-npm-package-name@npm:^5.0.0": + version: 5.0.0 + resolution: "validate-npm-package-name@npm:5.0.0" + dependencies: + builtins: ^5.0.0 + checksum: 5342a994986199b3c28e53a8452a14b2bb5085727691ea7aa0d284a6606b127c371e0925ae99b3f1ef7cc7d2c9de75f52eb61a3d1cc45e39bca1e3a9444cbb4e + languageName: node + linkType: hard + "validate-npm-package-name@npm:^3.0.0": version: 3.0.0 resolution: "validate-npm-package-name@npm:3.0.0" @@ -29868,15 +29439,6 @@ __metadata: languageName: node linkType: hard -"validate-npm-package-name@npm:^4.0.0": - version: 4.0.0 - resolution: "validate-npm-package-name@npm:4.0.0" - dependencies: - builtins: ^5.0.0 - checksum: a32fd537bad17fcb59cfd58ae95a414d443866020d448ec3b22e8d40550cb585026582a57efbe1f132b882eea4da8ac38ee35f7be0dd72988a3cb55d305a20c1 - languageName: node - linkType: hard - "value-equal@npm:^1.0.1": version: 1.0.1 resolution: "value-equal@npm:1.0.1" @@ -30056,13 +29618,6 @@ __metadata: languageName: node linkType: hard -"walk-up-path@npm:^1.0.0": - version: 1.0.0 - resolution: "walk-up-path@npm:1.0.0" - checksum: b8019ac4fb9ba1576839ec66d2217f62ab773c1cc4c704bfd1c79b1359fef5366f1382d3ab230a66a14c3adb1bf0fe102d1fdaa3437881e69154dfd1432abd32 - languageName: node - linkType: hard - "walker@npm:^1.0.8": version: 1.0.8 resolution: "walker@npm:1.0.8" @@ -30565,6 +30120,17 @@ __metadata: languageName: node linkType: hard +"which@npm:^3.0.0": + version: 3.0.1 + resolution: "which@npm:3.0.1" + dependencies: + isexe: ^2.0.0 + bin: + node-which: bin/which.js + checksum: adf720fe9d84be2d9190458194f814b5e9015ae4b88711b150f30d0f4d0b646544794b86f02c7ebeec1db2029bc3e83a7ff156f542d7521447e5496543e26890 + languageName: node + linkType: hard + "wide-align@npm:^1.1.0, wide-align@npm:^1.1.5": version: 1.1.5 resolution: "wide-align@npm:1.1.5" @@ -30635,6 +30201,16 @@ __metadata: languageName: node linkType: hard +"write-file-atomic@npm:5.0.1, write-file-atomic@npm:^5.0.1": + version: 5.0.1 + resolution: "write-file-atomic@npm:5.0.1" + dependencies: + imurmurhash: ^0.1.4 + signal-exit: ^4.0.1 + checksum: 8dbb0e2512c2f72ccc20ccedab9986c7d02d04039ed6e8780c987dc4940b793339c50172a1008eed7747001bfacc0ca47562668a069a7506c46c77d7ba3926a9 + languageName: node + linkType: hard + "write-file-atomic@npm:^2.3.0, write-file-atomic@npm:^2.4.2": version: 2.4.3 resolution: "write-file-atomic@npm:2.4.3" @@ -30646,19 +30222,7 @@ __metadata: languageName: node linkType: hard -"write-file-atomic@npm:^3.0.0": - version: 3.0.3 - resolution: "write-file-atomic@npm:3.0.3" - dependencies: - imurmurhash: ^0.1.4 - is-typedarray: ^1.0.0 - signal-exit: ^3.0.2 - typedarray-to-buffer: ^3.1.5 - checksum: c55b24617cc61c3a4379f425fc62a386cc51916a9b9d993f39734d005a09d5a4bb748bc251f1304e7abd71d0a26d339996c275955f527a131b1dcded67878280 - languageName: node - linkType: hard - -"write-file-atomic@npm:^4.0.0, write-file-atomic@npm:^4.0.1, write-file-atomic@npm:^4.0.2": +"write-file-atomic@npm:^4.0.2": version: 4.0.2 resolution: "write-file-atomic@npm:4.0.2" dependencies: @@ -30668,16 +30232,6 @@ __metadata: languageName: node linkType: hard -"write-file-atomic@npm:^5.0.1": - version: 5.0.1 - resolution: "write-file-atomic@npm:5.0.1" - dependencies: - imurmurhash: ^0.1.4 - signal-exit: ^4.0.1 - checksum: 8dbb0e2512c2f72ccc20ccedab9986c7d02d04039ed6e8780c987dc4940b793339c50172a1008eed7747001bfacc0ca47562668a069a7506c46c77d7ba3926a9 - languageName: node - linkType: hard - "write-json-file@npm:^3.2.0": version: 3.2.0 resolution: "write-json-file@npm:3.2.0" @@ -30692,21 +30246,7 @@ __metadata: languageName: node linkType: hard -"write-json-file@npm:^4.3.0": - version: 4.3.0 - resolution: "write-json-file@npm:4.3.0" - dependencies: - detect-indent: ^6.0.0 - graceful-fs: ^4.1.15 - is-plain-obj: ^2.0.0 - make-dir: ^3.0.0 - sort-keys: ^4.0.0 - write-file-atomic: ^3.0.0 - checksum: 33908c591923dc273e6574e7c0e2df157acfcf498e3a87c5615ced006a465c4058877df6abce6fc1acd2844fa3cf4518ace4a34d5d82ab28bcf896317ba1db6f - languageName: node - linkType: hard - -"write-pkg@npm:^4.0.0": +"write-pkg@npm:4.0.0": version: 4.0.0 resolution: "write-pkg@npm:4.0.0" dependencies: @@ -30870,10 +30410,10 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:21.0.1": - version: 21.0.1 - resolution: "yargs-parser@npm:21.0.1" - checksum: c3ea2ed12cad0377ce3096b3f138df8267edf7b1aa7d710cd502fe16af417bafe4443dd71b28158c22fcd1be5dfd0e86319597e47badf42ff83815485887323a +"yargs-parser@npm:21.1.1, yargs-parser@npm:^21.0.1, yargs-parser@npm:^21.1.1": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: ed2d96a616a9e3e1cc7d204c62ecc61f7aaab633dcbfab2c6df50f7f87b393993fe6640d017759fe112d0cb1e0119f2b4150a87305cc873fd90831c6a58ccf1c languageName: node linkType: hard @@ -30884,13 +30424,6 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^21.0.1, yargs-parser@npm:^21.1.1": - version: 21.1.1 - resolution: "yargs-parser@npm:21.1.1" - checksum: ed2d96a616a9e3e1cc7d204c62ecc61f7aaab633dcbfab2c6df50f7f87b393993fe6640d017759fe112d0cb1e0119f2b4150a87305cc873fd90831c6a58ccf1c - languageName: node - linkType: hard - "yargs-unparser@npm:2.0.0": version: 2.0.0 resolution: "yargs-unparser@npm:2.0.0" @@ -30918,7 +30451,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.3.1, yargs@npm:^17.4.0, yargs@npm:^17.5.1": +"yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: From 46f331e2848e9d85f61bf074aa637707e71ddffd Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Fri, 27 Oct 2023 13:00:49 +0200 Subject: [PATCH 126/180] Logs: remove toggleLabelsInLogsUI (#77264) * toggleLabelsInLogsUI: remove flag * Remove unused imports * isFilterLabelActive: refId is not optional * Revert "isFilterLabelActive: refId is not optional" This reverts commit 008931b7e9d068c34482ced164a42bd59160e850. * Revert method signature change * Update tests * Update tests --- .../feature-toggles/index.md | 1 - .../src/types/featureToggles.gen.ts | 1 - pkg/services/featuremgmt/registry.go | 8 ----- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 --- public/app/features/explore/Explore.tsx | 7 ++-- .../logs/components/LogDetails.test.tsx | 13 ++----- .../logs/components/LogDetailsRow.test.tsx | 34 +++++-------------- .../logs/components/LogDetailsRow.tsx | 22 +++++------- 9 files changed, 22 insertions(+), 69 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 7e091a115e2..8ccd3dbb81e 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -48,7 +48,6 @@ Some features are enabled by default. You can disable these feature by setting t | `cloudWatchLogsMonacoEditor` | Enables the Monaco editor for CloudWatch Logs queries | Yes | | `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries | Yes | | `transformationsRedesign` | Enables the transformations redesign | Yes | -| `toggleLabelsInLogsUI` | Enable toggleable filters in log details view | Yes | | `azureMonitorDataplane` | Adds dataplane compliant frame metadata in the Azure Monitor datasource | Yes | | `prometheusConfigOverhaulAuth` | Update the Prometheus configuration page with the new auth component | Yes | | `dashgpt` | Enable AI powered features in dashboards | Yes | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 8d905e8bfc1..1290d6503bb 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -103,7 +103,6 @@ export interface FeatureToggles { logsExploreTableVisualisation?: boolean; awsDatasourcesTempCredentials?: boolean; transformationsRedesign?: boolean; - toggleLabelsInLogsUI?: boolean; mlExpressions?: boolean; traceQLStreaming?: boolean; metricsSummary?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index d12d46d9dfe..89341a7077d 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -589,14 +589,6 @@ var ( Expression: "true", // enabled by default Owner: grafanaObservabilityMetricsSquad, }, - { - Name: "toggleLabelsInLogsUI", - Description: "Enable toggleable filters in log details view", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Expression: "true", // enabled by default - Owner: grafanaObservabilityLogsSquad, - }, { Name: "mlExpressions", Description: "Enable support for Machine Learning in server-side expressions", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index d94647febe3..00a269375c3 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -84,7 +84,6 @@ prometheusIncrementalQueryInstrumentation,experimental,@grafana/observability-me logsExploreTableVisualisation,experimental,@grafana/observability-logs,false,false,false,true awsDatasourcesTempCredentials,experimental,@grafana/aws-datasources,false,false,false,false transformationsRedesign,GA,@grafana/observability-metrics,false,false,false,true -toggleLabelsInLogsUI,GA,@grafana/observability-logs,false,false,false,true mlExpressions,experimental,@grafana/alerting-squad,false,false,false,false traceQLStreaming,experimental,@grafana/observability-traces-and-profiling,false,false,false,true metricsSummary,experimental,@grafana/observability-traces-and-profiling,false,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 7cdc48e682a..304b9a8dfe1 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -347,10 +347,6 @@ const ( // Enables the transformations redesign FlagTransformationsRedesign = "transformationsRedesign" - // FlagToggleLabelsInLogsUI - // Enable toggleable filters in log details view - FlagToggleLabelsInLogsUI = "toggleLabelsInLogsUI" - // FlagMlExpressions // Enable support for Machine Learning in server-side expressions FlagMlExpressions = "mlExpressions" diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 1fca13ba665..c41cbde0c0c 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -17,7 +17,7 @@ import { SupplementaryQueryType, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; +import { getDataSourceSrv, reportInteraction } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { AdHocFilterItem, @@ -206,9 +206,6 @@ export class Explore extends React.PureComponent { * @alpha */ isFilterLabelActive = async (key: string, value: string, refId?: string) => { - if (!config.featureToggles.toggleLabelsInLogsUI) { - return false; - } const query = this.props.queries.find((q) => q.refId === refId); if (!query) { return false; @@ -254,7 +251,7 @@ export class Explore extends React.PureComponent { return query; } const ds = await getDataSourceSrv().get(datasource); - if (hasToggleableQueryFiltersSupport(ds) && config.featureToggles.toggleLabelsInLogsUI) { + if (hasToggleableQueryFiltersSupport(ds)) { return ds.toggleQueryFilter(query, { type: modification.type === 'ADD_FILTER' ? 'FILTER_FOR' : 'FILTER_OUT', options: modification.options ?? {}, diff --git a/public/app/features/logs/components/LogDetails.test.tsx b/public/app/features/logs/components/LogDetails.test.tsx index 4d963491499..00e6aef9683 100644 --- a/public/app/features/logs/components/LogDetails.test.tsx +++ b/public/app/features/logs/components/LogDetails.test.tsx @@ -3,7 +3,6 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { Field, LogLevel, LogRowModel, MutableDataFrame, createTheme, FieldType } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { LogDetails, Props } from './LogDetails'; import { createLogRow } from './__mocks__/logRow'; @@ -57,16 +56,10 @@ describe('LogDetails', () => { }, { labels: { key1: 'label1' } } ); - expect(screen.getByLabelText('Filter for value')).toBeInTheDocument(); - expect(screen.getByLabelText('Filter out value')).toBeInTheDocument(); + expect(screen.getByLabelText('Filter for value in query A')).toBeInTheDocument(); + expect(screen.getByLabelText('Filter out value in query A')).toBeInTheDocument(); }); - describe('With toggleLabelsInLogsUI=true', () => { - beforeAll(() => { - config.featureToggles.toggleLabelsInLogsUI = true; - }); - afterAll(() => { - config.featureToggles.toggleLabelsInLogsUI = false; - }); + describe('Toggleable filters', () => { it('should provide the log row to Explore filter functions', async () => { const onClickFilterLabelMock = jest.fn(); const onClickFilterOutLabelMock = jest.fn(); diff --git a/public/app/features/logs/components/LogDetailsRow.test.tsx b/public/app/features/logs/components/LogDetailsRow.test.tsx index 55e01e99569..380c916655f 100644 --- a/public/app/features/logs/components/LogDetailsRow.test.tsx +++ b/public/app/features/logs/components/LogDetailsRow.test.tsx @@ -1,10 +1,8 @@ import { fireEvent, render, screen } from '@testing-library/react'; import React, { ComponentProps } from 'react'; -import { LogRowModel } from '@grafana/data'; -import config from 'app/core/config'; - import { LogDetailsRow } from './LogDetailsRow'; +import { createLogRow } from './__mocks__/logRow'; type Props = ComponentProps; @@ -20,7 +18,7 @@ const setup = (propOverrides?: Partial) => { onClickShowField: () => {}, onClickHideField: () => {}, displayedFields: [], - row: {} as LogRowModel, + row: createLogRow(), disableActions: false, }; @@ -55,32 +53,18 @@ describe('LogDetailsRow', () => { expect(screen.getAllByRole('button', { name: 'Ad-hoc statistics' })).toHaveLength(1); }); - describe('if props is a label', () => { - it('should render filter label button', () => { + describe('toggleable filters', () => { + it('should render filter buttons', () => { setup(); - expect(screen.getAllByRole('button', { name: 'Filter for value' })).toHaveLength(1); - expect(screen.queryByRole('button', { name: 'Remove filter' })).not.toBeInTheDocument(); + expect(screen.getAllByRole('button', { name: 'Filter for value in query A' })).toHaveLength(1); + expect(screen.getAllByRole('button', { name: 'Filter out value in query A' })).toHaveLength(1); + expect(screen.queryByRole('button', { name: 'Remove filter in query A' })).not.toBeInTheDocument(); }); - it('should render filter out label button', () => { - setup(); - expect(screen.getAllByRole('button', { name: 'Filter out value' })).toHaveLength(1); - }); - it('should render filter buttons when toggleLabelsInLogsUI false', async () => { + it('should render remove filter button when the filter is active', async () => { setup({ isFilterLabelActive: jest.fn().mockResolvedValue(true), }); - expect(screen.getByRole('button', { name: 'Filter for value' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Filter out value' })).toBeInTheDocument(); - }); - - it('should render remove filter button when toggleLabelsInLogsUI true', async () => { - const defaultValue = config.featureToggles.toggleLabelsInLogsUI; - config.featureToggles.toggleLabelsInLogsUI = true; - setup({ - isFilterLabelActive: jest.fn().mockResolvedValue(true), - }); - expect(await screen.findByRole('button', { name: 'Remove filter' })).toBeInTheDocument(); - config.featureToggles.toggleLabelsInLogsUI = defaultValue; + expect(await screen.findByRole('button', { name: 'Remove filter in query A' })).toBeInTheDocument(); }); }); diff --git a/public/app/features/logs/components/LogDetailsRow.tsx b/public/app/features/logs/components/LogDetailsRow.tsx index 210b1fc71d3..b96dd64d38c 100644 --- a/public/app/features/logs/components/LogDetailsRow.tsx +++ b/public/app/features/logs/components/LogDetailsRow.tsx @@ -4,7 +4,7 @@ import memoizeOne from 'memoize-one'; import React, { PureComponent, useState } from 'react'; import { CoreApp, Field, GrafanaTheme2, IconName, LinkModel, LogLabelStatsModel, LogRowModel } from '@grafana/data'; -import { config, reportInteraction } from '@grafana/runtime'; +import { reportInteraction } from '@grafana/runtime'; import { ClipboardButton, DataLinkButton, IconButton, Themeable2, withTheme2 } from '@grafana/ui'; import { LogLabelStats } from './LogLabelStats'; @@ -257,8 +257,7 @@ class UnThemedLogDetailsRow extends PureComponent { const singleKey = parsedKeys == null ? false : parsedKeys.length === 1; const singleVal = parsedValues == null ? false : parsedValues.length === 1; const hasFilteringFunctionality = !disableActions && onClickFilterLabel && onClickFilterOutLabel; - const refIdTooltip = - config.featureToggles.toggleLabelsInLogsUI && row.dataFrame?.refId ? ` in query ${row.dataFrame?.refId}` : ''; + const refIdTooltip = row.dataFrame?.refId ? ` in query ${row.dataFrame?.refId}` : ''; const isMultiParsedValueWithNoContent = !singleVal && parsedValues != null && !parsedValues.every((val) => val === ''); @@ -277,17 +276,12 @@ class UnThemedLogDetailsRow extends PureComponent {
{hasFilteringFunctionality && ( <> - {config.featureToggles.toggleLabelsInLogsUI ? ( - // If we are using the new label toggling, we want to use the async icon button - - ) : ( - - )} + Date: Fri, 27 Oct 2023 13:07:32 +0200 Subject: [PATCH 127/180] Chore: Add GH workflow to release core plugins (#77204) --- .github/CODEOWNERS | 2 +- .../core-plugins-build-and-release.yml | 218 ++++++++++++++++++ 2 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/core-plugins-build-and-release.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2a7d1df2adc..737b091cd7f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -655,7 +655,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/ephemeral-instances-pr-comment.yml @grafana/grafana-operator-experience-squad /.github/workflows/ephemeral-instances-pr-opened-closed.yml @grafana/grafana-operator-experience-squad /.github/workflows/create-security-patch-from-security-mirror.yml @grafana/grafana-delivery - +/.github/workflows/core-plugins-build-and-release.yml @grafana/plugins-platform-frontend @grafana/plugins-platform-backend # Generated files not requiring owner approval /packages/grafana-data/src/types/featureToggles.gen.ts @grafanabot diff --git a/.github/workflows/core-plugins-build-and-release.yml b/.github/workflows/core-plugins-build-and-release.yml new file mode 100644 index 00000000000..5dadec16321 --- /dev/null +++ b/.github/workflows/core-plugins-build-and-release.yml @@ -0,0 +1,218 @@ +on: + workflow_dispatch: + inputs: + plugin_id: + description: "ID of the plugin you want to publish" + required: true + type: choice + options: + - grafana-testdata-datasource + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}-${{ inputs.plugin_id }} + cancel-in-progress: true + +env: + GRABPL_VERSION: 3.0.44 + GCP_BUCKET: integration-artifacts # Dev: plugins-community-staging + GCOM_API: https://grafana.com # Dev: https://grafana-dev.com + +jobs: + build-and-publish: + name: Build and publish ${{ inputs.plugin_id }} + runs-on: ubuntu-latest + outputs: + type: ${{ steps.get_dir.outputs.dir }} + has_backend: ${{ steps.check_backend.outputs.has_backend }} + version: ${{ steps.build_frontend.outputs.version }} + steps: + - name: checkout + uses: actions/checkout@v3 + - name: Verify inputs + run: | + if [ -z ${{ inputs.plugin_id }} ]; then echo "Missing plugin ID"; exit 1; fi + - name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v1' + with: + credentials_json: '${{ secrets.PLUGINS_GOOGLE_CREDENTIALS }}' + - name: 'Set up Cloud SDK' + uses: 'google-github-actions/setup-gcloud@v1' + - name: Setup nodejs environment + uses: actions/setup-node@v3 + with: + node-version-file: .nvmrc + cache: yarn + - name: Find plugin directory + shell: bash + id: get_dir + run: | + dir=$(find public/app/plugins -name ${{ inputs.plugin_id }} -print -quit) + echo "dir=${dir}" >> $GITHUB_OUTPUT + - name: Install frontend dependencies + shell: bash + working-directory: ${{ steps.get_dir.outputs.dir }} + run: | + yarn install --immutable + - name: Download grabpl executable + shell: sh + working-directory: ${{ steps.get_dir.outputs.dir }} + run: | + [ ! -d ./bin ] && mkdir -pv ./bin || true + curl -fL -o ./bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v${{ env.GRABPL_VERSION }}/grabpl + chmod 0755 ./bin/grabpl + - name: Check backend + id: check_backend + shell: bash + run: | + if [ -d ./pkg/tsdb/${{ inputs.plugin_id }} ]; then + echo "has_backend=true" >> $GITHUB_OUTPUT + else + echo "has_backend=false" >> $GITHUB_OUTPUT + fi + - name: Setup golang environment + uses: actions/setup-go@v4 + if: steps.check_backend.outputs.has_backend == 'true' + with: + go-version-file: go.mod + - name: Install Mage + shell: bash + if: steps.check_backend.outputs.has_backend == 'true' + run: | + go install github.com/magefile/mage + - name: Check tools + shell: bash + working-directory: ${{ steps.get_dir.outputs.dir }} + run: | + echo "=======================================" + echo " Frontend tools" + echo "=======================================" + echo "-------- node version -----" + node --version + echo "-------- npm version -----" + npm --version + echo "-------- yarn version -----" + yarn --version + echo "=======================================" + echo " Misc tools" + echo "=======================================" + echo "-------- docker version -----" + docker --version + echo "-------- jq version -----" + jq --version + echo "-------- grabpl version -----" + ./bin/grabpl --version + echo "=======================================" + - name: Check backend tools + shell: bash + if: steps.check_backend.outputs.has_backend == 'true' + working-directory: ${{ steps.get_dir.outputs.dir }} + run: | + echo "=======================================" + echo " Backend tools" + echo "=======================================" + echo "-------- go version -----" + go version + echo "-------- mage version -----" + mage --version + echo "=======================================" + - name: build:frontend + shell: bash + id: build_frontend + run: | + command="plugin:build:commit" + if [ "$GITHUB_REF" != "refs/heads/main" ]; then + # Release branch, do not add commit hash to version + command="plugin:build" + fi + yarn $command --scope="@grafana-plugins/${{ inputs.plugin_id }}" + version=$(cat ${{ steps.get_dir.outputs.dir }}/dist/plugin.json | jq -r .info.version) + echo "version=${version}" >> $GITHUB_OUTPUT + - name: build:backend + if: steps.check_backend.outputs.has_backend == 'true' + shell: bash + env: + VERSION: ${{ steps.build_frontend.outputs.version }} + run: | + make build-plugin-go PLUGIN_ID=${{ inputs.plugin_id }} + - name: package + working-directory: ${{ steps.get_dir.outputs.dir }} + run: | + mkdir -p ci/jobs/package + bin/grabpl plugin package + env: + GRAFANA_API_KEY: ${{ secrets.PLUGINS_GRAFANA_API_KEY }} + PLUGIN_SIGNATURE_TYPE: grafana + - name: Check existing release + env: + GCOM_TOKEN: ${{ secrets.PLUGINS_GCOM_TOKEN }} + VERSION: ${{ steps.build_frontend.outputs.version }} + run: | + api_res=$(curl -X 'GET' -H "Authorization: Bearer $GCOM_TOKEN" \ + '${{ env.GCOM_API}}/api/plugins/${{ inputs.plugin_id }}?version=$VERSION' \ + -H 'accept: application/json') + api_res_code=$(echo $api_res | jq -r .code) + if [ "$api_res_code" = "NotFound" ]; then + echo "No existing release found" + else + echo "Expecting a missing release, got:" + echo $api_res + exit 1 + fi + - name: store build artifacts + uses: actions/upload-artifact@v3 + with: + name: build-artifacts + path: ${{ steps.get_dir.outputs.dir }}/ci/packages/*.zip + - name: Publish release to Google Cloud Storage + working-directory: ${{ steps.get_dir.outputs.dir }} + env: + VERSION: ${{ steps.build_frontend.outputs.version }} + run: | + echo "Publish release to Google Cloud Storage:" + touch ci/packages/windows ci/packages/darwin ci/packages/linux ci/packages/any + gsutil -m cp -r ci/packages/*windows* gs://${{ env.GCP_BUCKET }}/${{ inputs.plugin_id }}/release/${VERSION}/windows + gsutil -m cp -r ci/packages/*linux* gs://${{ env.GCP_BUCKET }}/${{ inputs.plugin_id }}/release/${VERSION}/linux + gsutil -m cp -r ci/packages/*darwin* gs://${{ env.GCP_BUCKET }}/${{ inputs.plugin_id }}/release/${VERSION}/darwin + gsutil -m cp -r ci/packages/*any* gs://${{ env.GCP_BUCKET }}/${{ inputs.plugin_id }}/release/${VERSION}/any + - name: Publish new plugin version on grafana.com + working-directory: ${{ steps.get_dir.outputs.dir }} + env: + GCOM_TOKEN: ${{ secrets.PLUGINS_GCOM_TOKEN }} + VERSION: ${{ steps.build_frontend.outputs.version }} + run: | + echo "Publish new plugin version on grafana.com:" + echo "Plugin version: ${VERSION}" + result=`curl -H "Authorization: Bearer $GCOM_TOKEN" -H "Content-Type: application/json" ${{ env.GCOM_API}}/api/plugins -d "{ + \"url\": \"https://github.com/grafana/grafana/tree/main/${{ steps.get_dir.outputs.dir }}\", + \"download\": { + \"linux-amd64\": { + \"url\": \"https://storage.googleapis.com/${{ env.GCP_BUCKET }}/${{ inputs.plugin_id }}/release/${VERSION}/linux/${{ inputs.plugin_id }}-${VERSION}.linux_amd64.zip\", + \"md5\": \"$(cat ci/packages/info-linux_amd64.json | jq -r .plugin.md5)\" + }, + \"linux-arm64\": { + \"url\": \"https://storage.googleapis.com/${{ env.GCP_BUCKET }}/${{ inputs.plugin_id }}/release/${VERSION}/linux/${{ inputs.plugin_id }}-${VERSION}.linux_arm64.zip\", + \"md5\": \"$(cat ci/packages/info-linux_arm64.json | jq -r .plugin.md5)\" + }, + \"linux-arm\": { + \"url\": \"https://storage.googleapis.com/${{ env.GCP_BUCKET }}/${{ inputs.plugin_id }}/release/${VERSION}/linux/${{ inputs.plugin_id }}-${VERSION}.linux_arm.zip\", + \"md5\": \"$(cat ci/packages/info-linux_arm.json | jq -r .plugin.md5)\" + }, + \"windows-amd64\": { + \"url\": \"https://storage.googleapis.com/${{ env.GCP_BUCKET }}/${{ inputs.plugin_id }}/release/${VERSION}/windows/${{ inputs.plugin_id }}-${VERSION}.windows_amd64.zip\", + \"md5\": \"$(cat ci/packages/info-windows_amd64.json | jq -r .plugin.md5)\" + }, + \"darwin-amd64\": { + \"url\": \"https://storage.googleapis.com/${{ env.GCP_BUCKET }}/${{ inputs.plugin_id }}/release/${VERSION}/darwin/${{ inputs.plugin_id }}-${VERSION}.darwin_amd64.zip\", + \"md5\": \"$(cat ci/packages/info-darwin_amd64.json | jq -r .plugin.md5)\" + }, + \"darwin-arm64\": { + \"url\": \"https://storage.googleapis.com/${{ env.GCP_BUCKET }}/${{ inputs.plugin_id }}/release/${VERSION}/darwin/${{ inputs.plugin_id }}-${VERSION}.darwin_arm64.zip\", + \"md5\": \"$(cat ci/packages/info-darwin_arm64.json | jq -r .plugin.md5)\" + } + } + }"` + if [[ "$(echo $result | jq -r .version)" == "null" ]]; then + echo "Failed to publish plugin version. Got:" + echo $result + exit 1 + fi \ No newline at end of file From 57335cb173382e9c892f437220885eae2121660b Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Fri, 27 Oct 2023 12:37:37 +0100 Subject: [PATCH 128/180] Team LBAC: Add validation/regex of teamheaders (#76905) * add validation of team header values w. regex * apply valid headers * refactor testcases to account for badly formatted json * refactoring to move validation code close to the validation itself * removed tes * Update pkg/api/datasources_test.go Co-authored-by: Alexander Zobnin * Update pkg/api/datasources.go Co-authored-by: Alexander Zobnin * review comments * review during pairing --------- Co-authored-by: Alexander Zobnin --- pkg/api/datasources.go | 67 ++++++++++---- pkg/api/datasources_test.go | 136 ++++++++++++++++++++++------- pkg/services/datasources/models.go | 15 ++++ 3 files changed, 171 insertions(+), 47 deletions(-) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 8da5961d4c3..d4fb40a7394 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "regexp" "sort" "strconv" "strings" @@ -351,28 +352,64 @@ func validateJSONData(ctx context.Context, jsonData *simplejson.Json, cfg *setti // Prevent adding a data source team header with a name that matches the auth proxy header name if features.IsEnabled(featuremgmt.FlagTeamHttpHeaders) { - teamHTTPHeadersJSON, err := datasources.GetTeamHTTPHeaders(jsonData) + err := validateTeamHTTPHeaderJSON(jsonData) if err != nil { - datasourcesLogger.Error("Unable to marshal TeamHTTPHeaders") - return errors.New("validation error, invalid format of TeamHTTPHeaders") - } - // whitelisting X-Prom-Label-Policy - for _, headers := range teamHTTPHeadersJSON { - for _, header := range headers { - // TODO: currently we only allow for X-Prom-Label-Policy header to be used by our proxy - for _, name := range []string{"X-Prom-Label-Policy"} { - if http.CanonicalHeaderKey(header.Header) != http.CanonicalHeaderKey(name) { - datasourcesLogger.Error("Cannot add a data source team header that is different than", "headerName", name) - return errors.New("validation error, invalid header name specified") - } - } - } + return err } } return nil } +// we only allow for now the following headers to be added to a data source team header +var validHeaders = []string{"X-Prom-Label-Policy"} + +func validateTeamHTTPHeaderJSON(jsonData *simplejson.Json) error { + teamHTTPHeadersJSON, err := datasources.GetTeamHTTPHeaders(jsonData) + if err != nil { + datasourcesLogger.Error("Unable to marshal TeamHTTPHeaders") + return errors.New("validation error, invalid format of TeamHTTPHeaders") + } + // whitelisting ValidHeaders + // each teams headers + for _, teamheaders := range teamHTTPHeadersJSON { + for _, header := range teamheaders { + if !contains(validHeaders, header.Header) { + datasourcesLogger.Error("Cannot add a data source team header that is different than", "headerName", header.Header) + return errors.New("validation error, invalid header name specified") + } + if !teamHTTPHeaderValueRegexMatch(header.Value) { + datasourcesLogger.Error("Cannot add a data source team header value with invalid value", "headerValue", header.Value) + return errors.New("validation error, invalid header value syntax") + } + } + } + return nil +} + +func contains(slice []string, value string) bool { + for _, v := range slice { + if http.CanonicalHeaderKey(v) == http.CanonicalHeaderKey(value) { + return true + } + } + return false +} + +// teamHTTPHeaderValueRegexMatch returns true if the header value matches the regex +// words separated by special characters +// namespace!="auth", env="prod", env!~"dev" +func teamHTTPHeaderValueRegexMatch(headervalue string) bool { + // link to regex: https://regex101.com/r/I8KhZz/1 + // 1234:{ name!="value",foo!~"bar" } + exp := `^\d+:{(?:\s*\w+\s*(?:=|!=|=~|!~)\s*\"\w+\"\s*,*)+}$` + reg, err := regexp.Compile(exp) + if err != nil { + return false + } + return reg.Match([]byte(strings.TrimSpace(headervalue))) +} + // swagger:route POST /datasources datasources addDataSource // // Create a data source. diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index de0bea99891..f3b9ee53b3b 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -3,6 +3,7 @@ package api import ( "context" "encoding/json" + "fmt" "io" "net/http" "strings" @@ -224,44 +225,90 @@ func TestUpdateDataSource_InvalidJSONData(t *testing.T) { // Using a team HTTP header whose name matches the name specified for auth proxy header should fail func TestUpdateDataSourceTeamHTTPHeaders_InvalidJSONData(t *testing.T) { - hs := &HTTPServer{ - DataSourcesService: &dataSourcesServiceMock{}, - Cfg: setting.NewCfg(), - Features: featuremgmt.WithFeatures(featuremgmt.FlagTeamHttpHeaders), - } - sc := setupScenarioContext(t, "/api/datasources/1234") - - data := datasources.TeamHTTPHeaders{ - "1234": []datasources.TeamHTTPHeader{ - // Authorization is used by the auth proxy - // As part of - // contexthandler.AuthHTTPHeaderListFromContext(ctx) - { - Header: "Authorization", - Value: "Could be anything", + tenantID := "1234" + testcases := []struct { + desc string + data datasources.TeamHTTPHeaders + want int + }{ + { + desc: "We should only allow for headers being X-Prom-Label-Policy", + data: datasources.TeamHTTPHeaders{tenantID: []datasources.TeamHTTPHeader{ + { + Header: "Authorization", + Value: "foo!=bar", + }, }, + }, + want: 400, + }, + { + desc: "Allowed header but no team id", + data: datasources.TeamHTTPHeaders{"": []datasources.TeamHTTPHeader{ + { + Header: "X-Prom-Label-Policy", + Value: "foo=bar", + }, + }, + }, + want: 400, + }, + { + desc: "Allowed team id and header name with invalid header values ", + data: datasources.TeamHTTPHeaders{tenantID: []datasources.TeamHTTPHeader{ + { + Header: "X-Prom-Label-Policy", + Value: "Bad value", + }, + }, + }, + want: 400, + }, + // Complete valid case, with team id, header name and header value + { + desc: "Allowed header and header values ", + data: datasources.TeamHTTPHeaders{tenantID: []datasources.TeamHTTPHeader{ + { + Header: "X-Prom-Label-Policy", + Value: `1234:{ name!="value",foo!~"bar" }`, + }, + }, + }, + want: 200, }, } + for _, tc := range testcases { + t.Run(tc.desc, func(t *testing.T) { + hs := &HTTPServer{ + DataSourcesService: &dataSourcesServiceMock{ + expectedDatasource: &datasources.DataSource{}, + }, + Cfg: setting.NewCfg(), + Features: featuremgmt.WithFeatures(featuremgmt.FlagTeamHttpHeaders), + accesscontrolService: actest.FakeService{}, + } + sc := setupScenarioContext(t, fmt.Sprintf("/api/datasources/%s", tenantID)) + hs.Cfg.AuthProxyEnabled = true - hs.Cfg.AuthProxyEnabled = true - jsonData := simplejson.New() - jsonData.Set("teamHttpHeaders", data) + jsonData := simplejson.New() + jsonData.Set("teamHttpHeaders", tc.data) + sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { + c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ + Name: "Test", + URL: "localhost:5432", + Access: "direct", + Type: "test", + JsonData: jsonData, + }) + c.SignedInUser = authedUserWithPermissions(1, 1, []ac.Permission{}) + return hs.AddDataSource(c) + })) - sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { - c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ - Name: "Test", - URL: "localhost:5432", - Access: "direct", - Type: "test", - JsonData: jsonData, + sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() + + assert.Equal(t, tc.want, sc.resp.Code) }) - c.SignedInUser = authedUserWithPermissions(1, 1, []ac.Permission{}) - return hs.AddDataSource(c) - })) - - sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() - - assert.Equal(t, 400, sc.resp.Code) + } } // Updating data sources with URLs not specifying protocol should work. @@ -434,6 +481,31 @@ func TestAPI_datasources_AccessControl(t *testing.T) { } } +// TeamHTTPHeaderValueRegexMatch returns a regex that can be used to check +func TestTeamHTTPHeaderValueRegexMatch(t *testing.T) { + testcases := []struct { + desc string + teamHeaderValue string + want bool + }{ + { + desc: "Should be valid regex match for team headervalue", + teamHeaderValue: `1234:{ name!="value",foo!~"bar" }`, + want: true, + }, + { + desc: "Should return false for incorrect header value", + teamHeaderValue: `1234:!="value",foo!~"bar" }`, + want: false, + }, + } + for _, tc := range testcases { + t.Run(tc.desc, func(t *testing.T) { + assert.Equal(t, tc.want, teamHTTPHeaderValueRegexMatch(tc.teamHeaderValue)) + }) + } +} + type dataSourcesServiceMock struct { datasources.DataSourceService diff --git a/pkg/services/datasources/models.go b/pkg/services/datasources/models.go index 3e4ccc4a5a9..699f40b3f26 100644 --- a/pkg/services/datasources/models.go +++ b/pkg/services/datasources/models.go @@ -2,6 +2,7 @@ package datasources import ( "encoding/json" + "errors" "time" "github.com/grafana/grafana/pkg/components/simplejson" @@ -92,6 +93,20 @@ func GetTeamHTTPHeaders(jsonData *simplejson.Json) (TeamHTTPHeaders, error) { if err != nil { return nil, err } + for teamID, headers := range teamHTTPHeadersJSON { + if teamID == "" { + return nil, errors.New("teamID is missing or empty in teamHttpHeaders") + } + + for _, header := range headers { + if header.Header == "" { + return nil, errors.New("header name is missing or empty") + } + if header.Value == "" { + return nil, errors.New("header value is missing or empty") + } + } + } } return teamHTTPHeadersJSON, nil From 2727f414741da70c8ac712adb4b6f9e50ff3bea5 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Fri, 27 Oct 2023 13:46:25 +0200 Subject: [PATCH 129/180] AuthN: Change the external service account state on plugin state change (#77157) * Disable plugin service account * Revert extsvc injection * handle plugin state changes * Use isProxyEnabled * Remove plugininteg changes * Change update function to also work for mysql :weary: * Change test to also check no collateral update * Update pkg/services/serviceaccounts/database/store_test.go * Update pkg/services/serviceaccounts/database/store_test.go --- .../serviceaccounts/database/store.go | 9 + .../serviceaccounts/database/store_test.go | 63 ++++ .../serviceaccounts/extsvcaccounts/service.go | 38 ++- .../serviceaccounts/manager/service.go | 10 + .../serviceaccounts/manager/service_test.go | 7 + pkg/services/serviceaccounts/manager/store.go | 21 +- pkg/services/serviceaccounts/models.go | 6 + pkg/services/serviceaccounts/proxy/service.go | 14 + .../serviceaccounts/serviceaccounts.go | 5 + .../serviceaccounts/tests/extsvcaccmock.go | 14 + pkg/services/serviceaccounts/tests/fakes.go | 4 + pkg/services/serviceaccounts/tests/mocks.go | 312 +++++++++++++++--- 12 files changed, 441 insertions(+), 62 deletions(-) diff --git a/pkg/services/serviceaccounts/database/store.go b/pkg/services/serviceaccounts/database/store.go index 5790b755baa..5621f5743a8 100644 --- a/pkg/services/serviceaccounts/database/store.go +++ b/pkg/services/serviceaccounts/database/store.go @@ -171,6 +171,15 @@ func (s *ServiceAccountsStoreImpl) deleteServiceAccount(sess *db.Session, orgId, return nil } +// EnableServiceAccount enable/disable service account +func (s *ServiceAccountsStoreImpl) EnableServiceAccount(ctx context.Context, orgID, serviceAccountID int64, enable bool) error { + return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { + query := "UPDATE " + s.sqlStore.GetDialect().Quote("user") + " SET is_disabled = ? WHERE id = ? AND is_service_account = ?" + _, err := sess.Exec(query, !enable, serviceAccountID, true) + return err + }) +} + // RetrieveServiceAccount returns a service account by its ID func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, orgId, serviceAccountId int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { serviceAccount := &serviceaccounts.ServiceAccountProfileDTO{} diff --git a/pkg/services/serviceaccounts/database/store_test.go b/pkg/services/serviceaccounts/database/store_test.go index e9e11d5f3ba..c3cb7782496 100644 --- a/pkg/services/serviceaccounts/database/store_test.go +++ b/pkg/services/serviceaccounts/database/store_test.go @@ -495,3 +495,66 @@ func TestServiceAccountsStoreImpl_SearchOrgServiceAccounts(t *testing.T) { }) } } + +func TestServiceAccountsStoreImpl_EnableServiceAccounts(t *testing.T) { + ctx := context.Background() + + initUsers := []tests.TestUser{ + {Name: "satest-1", Role: string(org.RoleViewer), Login: "sa-satest-1", IsServiceAccount: true}, + {Name: "satest-2", Role: string(org.RoleEditor), Login: "sa-satest-2", IsServiceAccount: true}, + {Name: "usertest-3", Role: string(org.RoleEditor), Login: "usertest-3", IsServiceAccount: false}, + } + + db, store := setupTestDatabase(t) + orgID := tests.SetupUsersServiceAccounts(t, db, initUsers) + + fetchStates := func() map[int64]bool { + sa1, err := store.RetrieveServiceAccount(ctx, orgID, 1) + require.NoError(t, err) + sa2, err := store.RetrieveServiceAccount(ctx, orgID, 2) + require.NoError(t, err) + user, err := store.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: 3}) + require.NoError(t, err) + return map[int64]bool{1: !sa1.IsDisabled, 2: !sa2.IsDisabled, 3: !user.IsDisabled} + } + + tt := []struct { + desc string + id int64 + enable bool + wantStates map[int64]bool + }{ + { + desc: "should disable service account", + id: 1, + enable: false, + wantStates: map[int64]bool{1: false, 2: true, 3: true}, + }, + { + desc: "should disable service account again", + id: 1, + enable: false, + wantStates: map[int64]bool{1: false, 2: true, 3: true}, + }, + { + desc: "should enable service account", + id: 1, + enable: true, + wantStates: map[int64]bool{1: true, 2: true, 3: true}, + }, + { + desc: "should not disable user", + id: 3, + enable: false, + wantStates: map[int64]bool{1: true, 2: true, 3: true}, + }, + } + for _, tc := range tt { + t.Run(tc.desc, func(t *testing.T) { + err := store.EnableServiceAccount(ctx, orgID, tc.id, tc.enable) + require.NoError(t, err) + + require.Equal(t, tc.wantStates, fetchStates()) + }) + } +} diff --git a/pkg/services/serviceaccounts/extsvcaccounts/service.go b/pkg/services/serviceaccounts/extsvcaccounts/service.go index ae29ccaedab..0562b5f74db 100644 --- a/pkg/services/serviceaccounts/extsvcaccounts/service.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/service.go @@ -6,6 +6,7 @@ import ( "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/satokengen" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" @@ -14,6 +15,7 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/extsvcauth" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/kvstore" sa "github.com/grafana/grafana/pkg/services/serviceaccounts" @@ -29,7 +31,7 @@ type ExtSvcAccountsService struct { skvStore kvstore.SecretsKVStore } -func ProvideExtSvcAccountsService(acSvc ac.Service, db db.DB, features *featuremgmt.FeatureManager, reg prometheus.Registerer, saSvc *manager.ServiceAccountsService, secretsSvc secrets.Service) *ExtSvcAccountsService { +func ProvideExtSvcAccountsService(acSvc ac.Service, bus bus.Bus, db db.DB, features *featuremgmt.FeatureManager, reg prometheus.Registerer, saSvc *manager.ServiceAccountsService, secretsSvc secrets.Service) *ExtSvcAccountsService { logger := log.New("serviceauth.extsvcaccounts") esa := &ExtSvcAccountsService{ acSvc: acSvc, @@ -39,14 +41,29 @@ func ProvideExtSvcAccountsService(acSvc ac.Service, db db.DB, features *featurem skvStore: kvstore.NewSQLSecretsKVStore(db, secretsSvc, logger), // Using SQL store to avoid a cyclic dependency } - // Register the metrics if features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) || features.IsEnabled(featuremgmt.FlagExternalServiceAuth) { + // Register the metrics esa.metrics = newMetrics(reg, saSvc, logger) + + // Register a listener to enable/disable service accounts + bus.AddEventListener(esa.handlePluginStateChanged) } return esa } +// EnableExtSvcAccount enables or disables the service account associated to an external service +func (esa *ExtSvcAccountsService) EnableExtSvcAccount(ctx context.Context, cmd *sa.EnableExtSvcAccountCmd) error { + saName := sa.ExtSvcPrefix + slugify.Slugify(cmd.ExtSvcSlug) + + saID, errRetrieve := esa.saSvc.RetrieveServiceAccountIdByName(ctx, cmd.OrgID, saName) + if errRetrieve != nil { + return errRetrieve + } + + return esa.saSvc.EnableServiceAccount(ctx, cmd.OrgID, saID, cmd.Enabled) +} + // RetrieveExtSvcAccount fetches an external service account by ID func (esa *ExtSvcAccountsService) RetrieveExtSvcAccount(ctx context.Context, orgID, saID int64) (*sa.ExtSvcAccount, error) { svcAcc, err := esa.saSvc.RetrieveServiceAccount(ctx, orgID, saID) @@ -266,3 +283,20 @@ func (esa *ExtSvcAccountsService) DeleteExtSvcCredentials(ctx context.Context, o esa.logger.Debug("Delete service account token from skv", "service", extSvcSlug, "orgID", orgID) return esa.skvStore.Del(ctx, orgID, extSvcSlug, kvStoreType) } + +func (esa *ExtSvcAccountsService) handlePluginStateChanged(ctx context.Context, event *pluginsettings.PluginStateChangedEvent) error { + esa.logger.Info("Plugin state changed", "pluginId", event.PluginId, "enabled", event.Enabled) + + errEnable := esa.EnableExtSvcAccount(ctx, &sa.EnableExtSvcAccountCmd{ + ExtSvcSlug: event.PluginId, + Enabled: event.Enabled, + OrgID: extsvcauth.TmpOrgID, + }) + + // Ignore service account not found error + if errors.Is(errEnable, sa.ErrServiceAccountNotFound) { + esa.logger.Debug("No ext svc account with this plugin", "pluginId", event.PluginId) + return nil + } + return errEnable +} diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index 0d037f230e2..8a9b557c145 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -183,6 +183,16 @@ func (sa *ServiceAccountsService) DeleteServiceAccount(ctx context.Context, orgI return sa.store.DeleteServiceAccount(ctx, orgID, serviceAccountID) } +func (sa *ServiceAccountsService) EnableServiceAccount(ctx context.Context, orgID, serviceAccountID int64, enable bool) error { + if err := validOrgID(orgID); err != nil { + return err + } + if err := validServiceAccountID(serviceAccountID); err != nil { + return err + } + return sa.store.EnableServiceAccount(ctx, orgID, serviceAccountID, enable) +} + func (sa *ServiceAccountsService) UpdateServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64, saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) { if err := validOrgID(orgID); err != nil { return nil, err diff --git a/pkg/services/serviceaccounts/manager/service_test.go b/pkg/services/serviceaccounts/manager/service_test.go index 31dce8ef1c4..bf0633bf1eb 100644 --- a/pkg/services/serviceaccounts/manager/service_test.go +++ b/pkg/services/serviceaccounts/manager/service_test.go @@ -25,6 +25,8 @@ type FakeServiceAccountStore struct { ExpectedError error } +var _ store = (*FakeServiceAccountStore)(nil) + func newServiceAccountStoreFake() *FakeServiceAccountStore { return &FakeServiceAccountStore{} } @@ -44,6 +46,11 @@ func (f *FakeServiceAccountStore) CreateServiceAccount(ctx context.Context, orgI return f.ExpectedServiceAccountDTO, f.ExpectedError } +// EnableServiceAccount implements store. +func (f *FakeServiceAccountStore) EnableServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64, enable bool) error { + return f.ExpectedError +} + // SearchOrgServiceAccounts is a fake searching for service accounts. func (f *FakeServiceAccountStore) SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) { return f.ExpectedSearchServiceAccountQueryResult, f.ExpectedError diff --git a/pkg/services/serviceaccounts/manager/store.go b/pkg/services/serviceaccounts/manager/store.go index 187ed7e0d86..b64e67d7522 100644 --- a/pkg/services/serviceaccounts/manager/store.go +++ b/pkg/services/serviceaccounts/manager/store.go @@ -8,18 +8,19 @@ import ( ) type store interface { + AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *serviceaccounts.AddServiceAccountTokenCommand) (*apikey.APIKey, error) CreateServiceAccount(ctx context.Context, orgID int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) + DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error + DeleteServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error + EnableServiceAccount(ctx context.Context, orgID, serviceAccountID int64, enable bool) error + GetUsageMetrics(ctx context.Context) (*serviceaccounts.Stats, error) + ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error) + MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error + MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error) + RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) + RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) + RevokeServiceAccountToken(ctx context.Context, orgId, serviceAccountId, tokenId int64) error SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) - RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) - RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) - DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error - MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error) - MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error - ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error) - RevokeServiceAccountToken(ctx context.Context, orgId, serviceAccountId, tokenId int64) error - AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *serviceaccounts.AddServiceAccountTokenCommand) (*apikey.APIKey, error) - DeleteServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error - GetUsageMetrics(ctx context.Context) (*serviceaccounts.Stats, error) } diff --git a/pkg/services/serviceaccounts/models.go b/pkg/services/serviceaccounts/models.go index 595818bc0e0..411b6f995b1 100644 --- a/pkg/services/serviceaccounts/models.go +++ b/pkg/services/serviceaccounts/models.go @@ -196,6 +196,12 @@ type ManageExtSvcAccountCmd struct { Permissions []accesscontrol.Permission } +type EnableExtSvcAccountCmd struct { + ExtSvcSlug string + Enabled bool + OrgID int64 +} + // AccessEvaluator is used to protect the "Configuration > Service accounts" page access var AccessEvaluator = accesscontrol.EvalAny( accesscontrol.EvalPermission(ActionRead), diff --git a/pkg/services/serviceaccounts/proxy/service.go b/pkg/services/serviceaccounts/proxy/service.go index 312fa8c2990..929a527189e 100644 --- a/pkg/services/serviceaccounts/proxy/service.go +++ b/pkg/services/serviceaccounts/proxy/service.go @@ -92,6 +92,20 @@ func (s *ServiceAccountsProxy) DeleteServiceAccountToken(ctx context.Context, or return s.proxiedService.DeleteServiceAccountToken(ctx, orgID, serviceAccountID, tokenID) } +func (s *ServiceAccountsProxy) EnableServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64, enable bool) error { + if s.isProxyEnabled { + sa, err := s.proxiedService.RetrieveServiceAccount(ctx, orgID, serviceAccountID) + if err != nil { + return err + } + if isExternalServiceAccount(sa.Login) { + s.log.Error("unable to enable/disable external service accounts", "serviceAccountID", serviceAccountID) + return extsvcaccounts.ErrCannotBeUpdated + } + } + return s.proxiedService.EnableServiceAccount(ctx, orgID, serviceAccountID, enable) +} + func (s *ServiceAccountsProxy) ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error) { return s.proxiedService.ListTokens(ctx, query) } diff --git a/pkg/services/serviceaccounts/serviceaccounts.go b/pkg/services/serviceaccounts/serviceaccounts.go index 535e2b50625..caef539eaaa 100644 --- a/pkg/services/serviceaccounts/serviceaccounts.go +++ b/pkg/services/serviceaccounts/serviceaccounts.go @@ -12,12 +12,15 @@ ServiceAccountService is the service that manages service accounts. Service accounts are used to authenticate API requests. They are not users and do not have a password. */ + +//go:generate mockery --name Service --structname MockServiceAccountService --output tests --outpkg tests --filename mocks.go type Service interface { CreateServiceAccount(ctx context.Context, orgID int64, saForm *CreateServiceAccountForm) (*ServiceAccountDTO, error) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*ServiceAccountProfileDTO, error) RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) SearchOrgServiceAccounts(ctx context.Context, query *SearchOrgServiceAccountsQuery) (*SearchOrgServiceAccountsResult, error) + EnableServiceAccount(ctx context.Context, orgID, serviceAccountID int64, enable bool) error UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, saForm *UpdateServiceAccountForm) (*ServiceAccountProfileDTO, error) @@ -34,6 +37,8 @@ type Service interface { //go:generate mockery --name ExtSvcAccountsService --structname MockExtSvcAccountsService --output tests --outpkg tests --filename extsvcaccmock.go type ExtSvcAccountsService interface { + // EnableExtSvcAccount enables or disables the service account associated to an external service + EnableExtSvcAccount(ctx context.Context, cmd *EnableExtSvcAccountCmd) error // ManageExtSvcAccount creates, updates or deletes the service account associated with an external service ManageExtSvcAccount(ctx context.Context, cmd *ManageExtSvcAccountCmd) (int64, error) // RetrieveExtSvcAccount fetches an external service account by ID diff --git a/pkg/services/serviceaccounts/tests/extsvcaccmock.go b/pkg/services/serviceaccounts/tests/extsvcaccmock.go index 62dd0ef4abf..50b8d9b56d3 100644 --- a/pkg/services/serviceaccounts/tests/extsvcaccmock.go +++ b/pkg/services/serviceaccounts/tests/extsvcaccmock.go @@ -14,6 +14,20 @@ type MockExtSvcAccountsService struct { mock.Mock } +// EnableExtSvcAccount provides a mock function with given fields: ctx, cmd +func (_m *MockExtSvcAccountsService) EnableExtSvcAccount(ctx context.Context, cmd *serviceaccounts.EnableExtSvcAccountCmd) error { + ret := _m.Called(ctx, cmd) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.EnableExtSvcAccountCmd) error); ok { + r0 = rf(ctx, cmd) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // ManageExtSvcAccount provides a mock function with given fields: ctx, cmd func (_m *MockExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *serviceaccounts.ManageExtSvcAccountCmd) (int64, error) { ret := _m.Called(ctx, cmd) diff --git a/pkg/services/serviceaccounts/tests/fakes.go b/pkg/services/serviceaccounts/tests/fakes.go index 016c1ec9585..df067ad5ae1 100644 --- a/pkg/services/serviceaccounts/tests/fakes.go +++ b/pkg/services/serviceaccounts/tests/fakes.go @@ -33,6 +33,10 @@ func (f *FakeServiceAccountService) DeleteServiceAccount(ctx context.Context, or return f.ExpectedErr } +func (f *FakeServiceAccountService) EnableServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64, enable bool) error { + return f.ExpectedErr +} + func (f *FakeServiceAccountService) RetrieveServiceAccount(ctx context.Context, orgID, id int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { return f.ExpectedServiceAccountProfile, f.ExpectedErr } diff --git a/pkg/services/serviceaccounts/tests/mocks.go b/pkg/services/serviceaccounts/tests/mocks.go index 3e310802f9d..5cd884fcf8d 100644 --- a/pkg/services/serviceaccounts/tests/mocks.go +++ b/pkg/services/serviceaccounts/tests/mocks.go @@ -1,82 +1,294 @@ +// Code generated by mockery v2.35.2. DO NOT EDIT. + package tests import ( - "context" + context "context" - "github.com/stretchr/testify/mock" + apikey "github.com/grafana/grafana/pkg/services/apikey" - "github.com/grafana/grafana/pkg/services/apikey" - "github.com/grafana/grafana/pkg/services/serviceaccounts" + mock "github.com/stretchr/testify/mock" + + serviceaccounts "github.com/grafana/grafana/pkg/services/serviceaccounts" ) -var _ serviceaccounts.Service = &MockServiceAccountService{} - +// MockServiceAccountService is an autogenerated mock type for the Service type type MockServiceAccountService struct { mock.Mock } -// AddServiceAccountToken implements serviceaccounts.Service -func (s *MockServiceAccountService) AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *serviceaccounts.AddServiceAccountTokenCommand) (*apikey.APIKey, error) { - mockedArgs := s.Called(ctx, serviceAccountID, cmd) - return mockedArgs.Get(0).(*apikey.APIKey), mockedArgs.Error(1) +// AddServiceAccountToken provides a mock function with given fields: ctx, serviceAccountID, cmd +func (_m *MockServiceAccountService) AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *serviceaccounts.AddServiceAccountTokenCommand) (*apikey.APIKey, error) { + ret := _m.Called(ctx, serviceAccountID, cmd) + + var r0 *apikey.APIKey + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, *serviceaccounts.AddServiceAccountTokenCommand) (*apikey.APIKey, error)); ok { + return rf(ctx, serviceAccountID, cmd) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, *serviceaccounts.AddServiceAccountTokenCommand) *apikey.APIKey); ok { + r0 = rf(ctx, serviceAccountID, cmd) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*apikey.APIKey) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, *serviceaccounts.AddServiceAccountTokenCommand) error); ok { + r1 = rf(ctx, serviceAccountID, cmd) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -// CreateServiceAccount implements serviceaccounts.Service -func (s *MockServiceAccountService) CreateServiceAccount(ctx context.Context, orgID int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { - mockedArgs := s.Called(ctx, orgID, saForm) - return mockedArgs.Get(0).(*serviceaccounts.ServiceAccountDTO), mockedArgs.Error(1) +// CreateServiceAccount provides a mock function with given fields: ctx, orgID, saForm +func (_m *MockServiceAccountService) CreateServiceAccount(ctx context.Context, orgID int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { + ret := _m.Called(ctx, orgID, saForm) + + var r0 *serviceaccounts.ServiceAccountDTO + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error)); ok { + return rf(ctx, orgID, saForm) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, *serviceaccounts.CreateServiceAccountForm) *serviceaccounts.ServiceAccountDTO); ok { + r0 = rf(ctx, orgID, saForm) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*serviceaccounts.ServiceAccountDTO) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, *serviceaccounts.CreateServiceAccountForm) error); ok { + r1 = rf(ctx, orgID, saForm) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -// DeleteServiceAccount implements serviceaccounts.Service -func (s *MockServiceAccountService) DeleteServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64) error { - mockedArgs := s.Called(ctx, orgID, serviceAccountID) - return mockedArgs.Error(0) +// DeleteServiceAccount provides a mock function with given fields: ctx, orgID, serviceAccountID +func (_m *MockServiceAccountService) DeleteServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64) error { + ret := _m.Called(ctx, orgID, serviceAccountID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok { + r0 = rf(ctx, orgID, serviceAccountID) + } else { + r0 = ret.Error(0) + } + + return r0 } -// DeleteServiceAccountToken implements serviceaccounts.Service -func (s *MockServiceAccountService) DeleteServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error { - mockedArgs := s.Called(ctx, orgID, serviceAccountID, tokenID) - return mockedArgs.Error(0) +// DeleteServiceAccountToken provides a mock function with given fields: ctx, orgID, serviceAccountID, tokenID +func (_m *MockServiceAccountService) DeleteServiceAccountToken(ctx context.Context, orgID int64, serviceAccountID int64, tokenID int64) error { + ret := _m.Called(ctx, orgID, serviceAccountID, tokenID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, int64, int64) error); ok { + r0 = rf(ctx, orgID, serviceAccountID, tokenID) + } else { + r0 = ret.Error(0) + } + + return r0 } -// ListTokens implements serviceaccounts.Service -func (s *MockServiceAccountService) ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error) { - mockedArgs := s.Called(ctx, query) - return mockedArgs.Get(0).([]apikey.APIKey), mockedArgs.Error(1) +// EnableServiceAccount provides a mock function with given fields: ctx, orgID, serviceAccountID, enable +func (_m *MockServiceAccountService) EnableServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64, enable bool) error { + ret := _m.Called(ctx, orgID, serviceAccountID, enable) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, int64, bool) error); ok { + r0 = rf(ctx, orgID, serviceAccountID, enable) + } else { + r0 = ret.Error(0) + } + + return r0 } -// MigrateApiKey implements serviceaccounts.Service -func (s *MockServiceAccountService) MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error { - mockedArgs := s.Called(ctx, orgID, keyId) - return mockedArgs.Error(0) +// ListTokens provides a mock function with given fields: ctx, query +func (_m *MockServiceAccountService) ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error) { + ret := _m.Called(ctx, query) + + var r0 []apikey.APIKey + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error)); ok { + return rf(ctx, query) + } + if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.GetSATokensQuery) []apikey.APIKey); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]apikey.APIKey) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *serviceaccounts.GetSATokensQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -// MigrateApiKeysToServiceAccounts implements serviceaccounts.Service -func (s *MockServiceAccountService) MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error) { - mockedArgs := s.Called(ctx, orgID) - return mockedArgs.Get(0).(*serviceaccounts.MigrationResult), mockedArgs.Error(1) +// MigrateApiKey provides a mock function with given fields: ctx, orgID, keyId +func (_m *MockServiceAccountService) MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error { + ret := _m.Called(ctx, orgID, keyId) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok { + r0 = rf(ctx, orgID, keyId) + } else { + r0 = ret.Error(0) + } + + return r0 } -// RetrieveServiceAccount implements serviceaccounts.Service -func (s *MockServiceAccountService) RetrieveServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { - mockedArgs := s.Called(ctx, orgID, serviceAccountID) - return mockedArgs.Get(0).(*serviceaccounts.ServiceAccountProfileDTO), mockedArgs.Error(1) +// MigrateApiKeysToServiceAccounts provides a mock function with given fields: ctx, orgID +func (_m *MockServiceAccountService) MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error) { + ret := _m.Called(ctx, orgID) + + var r0 *serviceaccounts.MigrationResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (*serviceaccounts.MigrationResult, error)); ok { + return rf(ctx, orgID) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) *serviceaccounts.MigrationResult); ok { + r0 = rf(ctx, orgID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*serviceaccounts.MigrationResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, orgID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -// RetrieveServiceAccountIdByName implements serviceaccounts.Service -func (s *MockServiceAccountService) RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) { - mockedArgs := s.Called(ctx, orgID, name) - return mockedArgs.Get(0).(int64), mockedArgs.Error(1) +// RetrieveServiceAccount provides a mock function with given fields: ctx, orgID, serviceAccountID +func (_m *MockServiceAccountService) RetrieveServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { + ret := _m.Called(ctx, orgID, serviceAccountID) + + var r0 *serviceaccounts.ServiceAccountProfileDTO + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, int64) (*serviceaccounts.ServiceAccountProfileDTO, error)); ok { + return rf(ctx, orgID, serviceAccountID) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, int64) *serviceaccounts.ServiceAccountProfileDTO); ok { + r0 = rf(ctx, orgID, serviceAccountID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*serviceaccounts.ServiceAccountProfileDTO) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, int64) error); ok { + r1 = rf(ctx, orgID, serviceAccountID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -// SearchOrgServiceAccounts implements serviceaccounts.Service -func (s *MockServiceAccountService) SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) { - mockedArgs := s.Called(ctx, query) - return mockedArgs.Get(0).(*serviceaccounts.SearchOrgServiceAccountsResult), mockedArgs.Error(1) +// RetrieveServiceAccountIdByName provides a mock function with given fields: ctx, orgID, name +func (_m *MockServiceAccountService) RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) { + ret := _m.Called(ctx, orgID, name) + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, string) (int64, error)); ok { + return rf(ctx, orgID, name) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, string) int64); ok { + r0 = rf(ctx, orgID, name) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok { + r1 = rf(ctx, orgID, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -// UpdateServiceAccount implements serviceaccounts.Service -func (s *MockServiceAccountService) UpdateServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64, saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) { - mockedArgs := s.Called(ctx, orgID, serviceAccountID) - return mockedArgs.Get(0).(*serviceaccounts.ServiceAccountProfileDTO), mockedArgs.Error(1) +// SearchOrgServiceAccounts provides a mock function with given fields: ctx, query +func (_m *MockServiceAccountService) SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) { + ret := _m.Called(ctx, query) + + var r0 *serviceaccounts.SearchOrgServiceAccountsResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error)); ok { + return rf(ctx, query) + } + if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.SearchOrgServiceAccountsQuery) *serviceaccounts.SearchOrgServiceAccountsResult); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*serviceaccounts.SearchOrgServiceAccountsResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *serviceaccounts.SearchOrgServiceAccountsQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateServiceAccount provides a mock function with given fields: ctx, orgID, serviceAccountID, saForm +func (_m *MockServiceAccountService) UpdateServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64, saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) { + ret := _m.Called(ctx, orgID, serviceAccountID, saForm) + + var r0 *serviceaccounts.ServiceAccountProfileDTO + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, int64, *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error)); ok { + return rf(ctx, orgID, serviceAccountID, saForm) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, int64, *serviceaccounts.UpdateServiceAccountForm) *serviceaccounts.ServiceAccountProfileDTO); ok { + r0 = rf(ctx, orgID, serviceAccountID, saForm) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*serviceaccounts.ServiceAccountProfileDTO) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, int64, *serviceaccounts.UpdateServiceAccountForm) error); ok { + r1 = rf(ctx, orgID, serviceAccountID, saForm) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewMockServiceAccountService creates a new instance of MockServiceAccountService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockServiceAccountService(t interface { + mock.TestingT + Cleanup(func()) +}) *MockServiceAccountService { + mock := &MockServiceAccountService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock } From 25b30aeb6d4309fcbd3391c104a55346d57023a3 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Fri, 27 Oct 2023 14:27:06 +0200 Subject: [PATCH 130/180] Plugin: Enable service account based on plugin settings on init (#77193) * Disable plugin service account * Revert extsvc injection * handle plugin state changes * Use isProxyEnabled * Remove plugininteg changes * Change update function to also work for mysql :weary: * Plugin: enable service account based on plugin settings on initialization * Remove misleading comment * Fix tests * test message * Clean up tests * Simplify tests * Re-order imports * Remove unecessary comment * Enable datasource plugins by default Co-authored-by: Andres Martinez Gotor --------- Co-authored-by: Andres Martinez Gotor --- pkg/plugins/auth/models.go | 2 +- pkg/plugins/manager/fakes/fakes.go | 2 +- .../pluginsintegration/pipeline/steps.go | 3 +- .../serviceregistration.go | 28 ++- .../serviceaccounts/extsvcaccounts/models.go | 1 + .../serviceaccounts/extsvcaccounts/service.go | 12 +- .../extsvcaccounts/service_test.go | 217 ++++++++---------- pkg/services/serviceaccounts/models.go | 2 +- 8 files changed, 130 insertions(+), 137 deletions(-) diff --git a/pkg/plugins/auth/models.go b/pkg/plugins/auth/models.go index 8c0f902203a..add4b46d504 100644 --- a/pkg/plugins/auth/models.go +++ b/pkg/plugins/auth/models.go @@ -13,5 +13,5 @@ type ExternalService struct { } type ExternalServiceRegistry interface { - RegisterExternalService(ctx context.Context, name string, svc *plugindef.ExternalServiceRegistration) (*ExternalService, error) + RegisterExternalService(ctx context.Context, name string, pType plugindef.Type, svc *plugindef.ExternalServiceRegistration) (*ExternalService, error) } diff --git a/pkg/plugins/manager/fakes/fakes.go b/pkg/plugins/manager/fakes/fakes.go index 908a3cfd506..60ac22c8d1c 100644 --- a/pkg/plugins/manager/fakes/fakes.go +++ b/pkg/plugins/manager/fakes/fakes.go @@ -437,7 +437,7 @@ type FakeAuthService struct { Result *auth.ExternalService } -func (f *FakeAuthService) RegisterExternalService(ctx context.Context, name string, svc *plugindef.ExternalServiceRegistration) (*auth.ExternalService, error) { +func (f *FakeAuthService) RegisterExternalService(ctx context.Context, name string, pType plugindef.Type, svc *plugindef.ExternalServiceRegistration) (*auth.ExternalService, error) { return f.Result, nil } diff --git a/pkg/services/pluginsintegration/pipeline/steps.go b/pkg/services/pluginsintegration/pipeline/steps.go index 44bd572dba3..97354b9e06f 100644 --- a/pkg/services/pluginsintegration/pipeline/steps.go +++ b/pkg/services/pluginsintegration/pipeline/steps.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation" "github.com/grafana/grafana/pkg/plugins/manager/signature" + "github.com/grafana/grafana/pkg/plugins/plugindef" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" ) @@ -40,7 +41,7 @@ func newExternalServiceRegistration(cfg *config.Cfg, serviceRegistry auth.Extern func (r *ExternalServiceRegistration) Register(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { if p.ExternalServiceRegistration != nil && (r.cfg.Features.IsEnabled(featuremgmt.FlagExternalServiceAuth) || r.cfg.Features.IsEnabled(featuremgmt.FlagExternalServiceAccounts)) { - s, err := r.externalServiceRegistry.RegisterExternalService(ctx, p.ID, p.ExternalServiceRegistration) + s, err := r.externalServiceRegistry.RegisterExternalService(ctx, p.ID, plugindef.Type(p.Type), p.ExternalServiceRegistration) if err != nil { r.log.Error("Could not register an external service. Initialization skipped", "pluginId", p.ID, "error", err) return nil, err diff --git a/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go b/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go index 396e43f2565..722891c111e 100644 --- a/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go +++ b/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go @@ -2,26 +2,42 @@ package serviceregistration import ( "context" + "errors" "github.com/grafana/grafana/pkg/plugins/auth" "github.com/grafana/grafana/pkg/plugins/plugindef" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/extsvcauth" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" ) type Service struct { - os extsvcauth.ExternalServiceRegistry + reg extsvcauth.ExternalServiceRegistry + settingsSvc pluginsettings.Service } -func ProvideService(os extsvcauth.ExternalServiceRegistry) *Service { +func ProvideService(reg extsvcauth.ExternalServiceRegistry, settingsSvc pluginsettings.Service) *Service { s := &Service{ - os: os, + reg: reg, + settingsSvc: settingsSvc, } return s } // RegisterExternalService is a simplified wrapper around SaveExternalService for the plugin use case. -func (s *Service) RegisterExternalService(ctx context.Context, svcName string, svc *plugindef.ExternalServiceRegistration) (*auth.ExternalService, error) { +func (s *Service) RegisterExternalService(ctx context.Context, svcName string, pType plugindef.Type, svc *plugindef.ExternalServiceRegistration) (*auth.ExternalService, error) { + // Datasource plugins can only be enabled + enabled := true + // App plugins can be disabled + if pType == plugindef.TypeApp { + settings, err := s.settingsSvc.GetPluginSettingByPluginID(ctx, &pluginsettings.GetByPluginIDArgs{PluginID: svcName}) + if err != nil && !errors.Is(err, pluginsettings.ErrPluginSettingNotFound) { + return nil, err + } + + enabled = (settings != nil) && settings.Enabled + } + impersonation := extsvcauth.ImpersonationCfg{} if svc.Impersonation != nil { impersonation.Permissions = toAccessControlPermissions(svc.Impersonation.Permissions) @@ -38,9 +54,9 @@ func (s *Service) RegisterExternalService(ctx context.Context, svcName string, s } self := extsvcauth.SelfCfg{} + self.Enabled = enabled if len(svc.Permissions) > 0 { self.Permissions = toAccessControlPermissions(svc.Permissions) - self.Enabled = true } registration := &extsvcauth.ExternalServiceRegistration{ @@ -56,7 +72,7 @@ func (s *Service) RegisterExternalService(ctx context.Context, svcName string, s registration.OAuthProviderCfg = &extsvcauth.OAuthProviderCfg{Key: &extsvcauth.KeyOption{Generate: true}} } - extSvc, err := s.os.SaveExternalService(ctx, registration) + extSvc, err := s.reg.SaveExternalService(ctx, registration) if err != nil || extSvc == nil { return nil, err } diff --git a/pkg/services/serviceaccounts/extsvcaccounts/models.go b/pkg/services/serviceaccounts/extsvcaccounts/models.go index 94154cddc0d..e10df1bd261 100644 --- a/pkg/services/serviceaccounts/extsvcaccounts/models.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/models.go @@ -36,6 +36,7 @@ type SaveCredentialsCmd struct { } type saveCmd struct { + Enabled bool ExtSvcSlug string OrgID int64 Permissions []ac.Permission diff --git a/pkg/services/serviceaccounts/extsvcaccounts/service.go b/pkg/services/serviceaccounts/extsvcaccounts/service.go index 0562b5f74db..24d00430e3c 100644 --- a/pkg/services/serviceaccounts/extsvcaccounts/service.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/service.go @@ -144,7 +144,7 @@ func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd * return 0, errRetrieve } - if !cmd.Enabled || len(cmd.Permissions) == 0 { + if len(cmd.Permissions) == 0 { if saID > 0 { if err := esa.deleteExtSvcAccount(ctx, cmd.OrgID, cmd.ExtSvcSlug, saID); err != nil { esa.logger.Error("Error occurred while deleting service account", @@ -155,15 +155,15 @@ func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd * } esa.metrics.deletedCount.Inc() } - esa.logger.Info("Skipping service account creation", + esa.logger.Info("Skipping service account creation, no permission", "service", cmd.ExtSvcSlug, - "enabled", cmd.Enabled, "permission count", len(cmd.Permissions), "saID", saID) return 0, nil } saID, errSave := esa.saveExtSvcAccount(ctx, &saveCmd{ + Enabled: cmd.Enabled, ExtSvcSlug: cmd.ExtSvcSlug, OrgID: cmd.OrgID, Permissions: cmd.Permissions, @@ -194,6 +194,12 @@ func (esa *ExtSvcAccountsService) saveExtSvcAccount(ctx context.Context, cmd *sa cmd.SaID = sa.Id } + // Enable or disable the service account + esa.logger.Debug("Set service account state", "service", cmd.ExtSvcSlug, "saID", cmd.SaID, "enabled", cmd.Enabled) + if err := esa.saSvc.EnableServiceAccount(ctx, cmd.OrgID, cmd.SaID, cmd.Enabled); err != nil { + return 0, err + } + // update the service account's permissions esa.logger.Debug("Update role permissions", "service", cmd.ExtSvcSlug, "saID", cmd.SaID) if err := esa.acSvc.SaveExternalServiceRole(ctx, ac.SaveExternalServiceRoleCommand{ diff --git a/pkg/services/serviceaccounts/extsvcaccounts/service_test.go b/pkg/services/serviceaccounts/extsvcaccounts/service_test.go index 4adfd06f3ae..636c0287ac7 100644 --- a/pkg/services/serviceaccounts/extsvcaccounts/service_test.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/service_test.go @@ -70,17 +70,23 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { name string init func(env *TestEnv) cmd sa.ManageExtSvcAccountCmd - checks func(t *testing.T, env *TestEnv) want int64 wantErr bool }{ { - name: "should remove service account when disabled", + name: "should disable service account", init: func(env *TestEnv) { // A previous service account was attached to this slug - env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, mock.Anything, mock.Anything).Return(extSvcAccID, nil) - env.SaSvc.On("DeleteServiceAccount", mock.Anything, mock.Anything, mock.Anything).Return(nil) - env.AcStore.On("DeleteExternalServiceRole", mock.Anything, mock.Anything).Return(nil) + env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, extSvcOrgID, sa.ExtSvcPrefix+extSvcSlug).Return(extSvcAccID, nil) + env.SaSvc.On("EnableServiceAccount", mock.Anything, extSvcOrgID, extSvcAccID, false).Return(nil) + env.AcStore.On("SaveExternalServiceRole", + mock.Anything, + mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool { + return cmd.ServiceAccountID == extSvcAccID && cmd.ExternalServiceID == extSvcSlug && + cmd.OrgID == int64(ac.GlobalOrgID) && len(cmd.Permissions) == 1 && + cmd.Permissions[0] == extSvcPerms[0] + })). + Return(nil) }, cmd: sa.ManageExtSvcAccountCmd{ ExtSvcSlug: extSvcSlug, @@ -88,26 +94,16 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { OrgID: extSvcOrgID, Permissions: extSvcPerms, }, - checks: func(t *testing.T, env *TestEnv) { - env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, - mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }), - mock.MatchedBy(func(slug string) bool { return slug == sa.ExtSvcPrefix+extSvcSlug })) - env.SaSvc.AssertCalled(t, "DeleteServiceAccount", mock.Anything, - mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }), - mock.MatchedBy(func(saID int64) bool { return saID == extSvcAccID })) - env.AcStore.AssertCalled(t, "DeleteExternalServiceRole", mock.Anything, - mock.MatchedBy(func(slug string) bool { return slug == extSvcSlug })) - }, - want: 0, + want: extSvcAccID, wantErr: false, }, { name: "should remove service account when no permission", init: func(env *TestEnv) { // A previous service account was attached to this slug - env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, mock.Anything, mock.Anything).Return(extSvcAccID, nil) - env.SaSvc.On("DeleteServiceAccount", mock.Anything, mock.Anything, mock.Anything).Return(nil) - env.AcStore.On("DeleteExternalServiceRole", mock.Anything, mock.Anything).Return(nil) + env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, extSvcOrgID, sa.ExtSvcPrefix+extSvcSlug).Return(extSvcAccID, nil) + env.SaSvc.On("DeleteServiceAccount", mock.Anything, extSvcOrgID, extSvcAccID).Return(nil) + env.AcStore.On("DeleteExternalServiceRole", mock.Anything, extSvcSlug).Return(nil) }, cmd: sa.ManageExtSvcAccountCmd{ ExtSvcSlug: extSvcSlug, @@ -115,16 +111,6 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { OrgID: extSvcOrgID, Permissions: []ac.Permission{}, }, - checks: func(t *testing.T, env *TestEnv) { - env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, - mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }), - mock.MatchedBy(func(slug string) bool { return slug == sa.ExtSvcPrefix+extSvcSlug })) - env.SaSvc.AssertCalled(t, "DeleteServiceAccount", mock.Anything, - mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }), - mock.MatchedBy(func(saID int64) bool { return saID == extSvcAccID })) - env.AcStore.AssertCalled(t, "DeleteExternalServiceRole", mock.Anything, - mock.MatchedBy(func(slug string) bool { return slug == extSvcSlug })) - }, want: 0, wantErr: false, }, @@ -132,11 +118,24 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { name: "should create new service account", init: func(env *TestEnv) { // No previous service account was attached to this slug - env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, mock.Anything, mock.Anything). + env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, extSvcOrgID, sa.ExtSvcPrefix+extSvcSlug). Return(int64(0), sa.ErrServiceAccountNotFound.Errorf("mock")) - env.SaSvc.On("CreateServiceAccount", mock.Anything, mock.Anything, mock.Anything). + env.SaSvc.On("CreateServiceAccount", + mock.Anything, + extSvcOrgID, + mock.MatchedBy(func(cmd *sa.CreateServiceAccountForm) bool { + return cmd.Name == sa.ExtSvcPrefix+extSvcSlug && *cmd.Role == roletype.RoleNone + })). Return(extSvcAccount, nil) - env.AcStore.On("SaveExternalServiceRole", mock.Anything, mock.Anything).Return(nil) + env.SaSvc.On("EnableServiceAccount", mock.Anything, extSvcOrgID, extSvcAccount.Id, true).Return(nil) + env.AcStore.On("SaveExternalServiceRole", + mock.Anything, + mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool { + return cmd.ServiceAccountID == extSvcAccount.Id && cmd.ExternalServiceID == extSvcSlug && + cmd.OrgID == int64(ac.GlobalOrgID) && len(cmd.Permissions) == 1 && + cmd.Permissions[0] == extSvcPerms[0] + })). + Return(nil) }, cmd: sa.ManageExtSvcAccountCmd{ ExtSvcSlug: extSvcSlug, @@ -144,23 +143,6 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { OrgID: extSvcOrgID, Permissions: extSvcPerms, }, - checks: func(t *testing.T, env *TestEnv) { - env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, - mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }), - mock.MatchedBy(func(slug string) bool { return slug == sa.ExtSvcPrefix+extSvcSlug })) - env.SaSvc.AssertCalled(t, "CreateServiceAccount", mock.Anything, - mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }), - mock.MatchedBy(func(cmd *sa.CreateServiceAccountForm) bool { - return cmd.Name == sa.ExtSvcPrefix+extSvcSlug && *cmd.Role == roletype.RoleNone - }), - ) - env.AcStore.AssertCalled(t, "SaveExternalServiceRole", mock.Anything, - mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool { - return cmd.ServiceAccountID == extSvcAccount.Id && cmd.ExternalServiceID == extSvcSlug && - cmd.OrgID == int64(ac.GlobalOrgID) && len(cmd.Permissions) == 1 && - cmd.Permissions[0] == extSvcPerms[0] - })) - }, want: extSvcAccID, wantErr: false, }, @@ -168,9 +150,17 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { name: "should update service account", init: func(env *TestEnv) { // A previous service account was attached to this slug - env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, mock.Anything, mock.Anything). + env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, extSvcOrgID, sa.ExtSvcPrefix+extSvcSlug). Return(int64(11), nil) - env.AcStore.On("SaveExternalServiceRole", mock.Anything, mock.Anything).Return(nil) + env.SaSvc.On("EnableServiceAccount", mock.Anything, extSvcOrgID, int64(11), true).Return(nil) + env.AcStore.On("SaveExternalServiceRole", + mock.Anything, + mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool { + return cmd.ServiceAccountID == int64(11) && cmd.ExternalServiceID == extSvcSlug && + cmd.OrgID == int64(ac.GlobalOrgID) && len(cmd.Permissions) == 1 && + cmd.Permissions[0] == extSvcPerms[0] + })). + Return(nil) }, cmd: sa.ManageExtSvcAccountCmd{ ExtSvcSlug: extSvcSlug, @@ -178,17 +168,6 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { OrgID: extSvcOrgID, Permissions: extSvcPerms, }, - checks: func(t *testing.T, env *TestEnv) { - env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, - mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }), - mock.MatchedBy(func(slug string) bool { return slug == sa.ExtSvcPrefix+extSvcSlug })) - env.AcStore.AssertCalled(t, "SaveExternalServiceRole", mock.Anything, - mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool { - return cmd.ServiceAccountID == int64(11) && cmd.ExternalServiceID == extSvcSlug && - cmd.OrgID == int64(ac.GlobalOrgID) && len(cmd.Permissions) == 1 && - cmd.Permissions[0] == extSvcPerms[0] - })) - }, want: 11, wantErr: false, }, @@ -210,10 +189,6 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { } require.NoError(t, err) - if tt.checks != nil { - tt.checks(t, env) - } - require.Equal(t, tt.want, got) }) } @@ -242,12 +217,20 @@ func TestExtSvcAccountsService_SaveExternalService(t *testing.T) { wantErr bool }{ { - name: "should remove service account when disabled", + name: "should disable service account", init: func(env *TestEnv) { // A previous service account was attached to this slug - env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, mock.Anything, mock.Anything).Return(extSvcAccID, nil) - env.SaSvc.On("DeleteServiceAccount", mock.Anything, mock.Anything, mock.Anything).Return(nil) - env.AcStore.On("DeleteExternalServiceRole", mock.Anything, mock.Anything).Return(nil) + env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, tmpOrgID, sa.ExtSvcPrefix+extSvcSlug). + Return(extSvcAccID, nil) + env.SaSvc.On("EnableServiceAccount", mock.Anything, tmpOrgID, extSvcAccID, false).Return(nil) + env.AcStore.On("SaveExternalServiceRole", + mock.Anything, + mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool { + return cmd.ServiceAccountID == extSvcAccID && cmd.ExternalServiceID == extSvcSlug && + cmd.OrgID == int64(ac.GlobalOrgID) && len(cmd.Permissions) == 1 && + cmd.Permissions[0] == extSvcPerms[0] + })). + Return(nil) // A token was previously stored in the secret store _ = env.SkvStore.Set(context.Background(), tmpOrgID, extSvcSlug, kvStoreType, "ExtSvcSecretToken") }, @@ -259,27 +242,26 @@ func TestExtSvcAccountsService_SaveExternalService(t *testing.T) { }, }, checks: func(t *testing.T, env *TestEnv) { - env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, - mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), - mock.MatchedBy(func(slug string) bool { return slug == sa.ExtSvcPrefix+extSvcSlug })) - env.SaSvc.AssertCalled(t, "DeleteServiceAccount", mock.Anything, - mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), - mock.MatchedBy(func(saID int64) bool { return saID == extSvcAccID })) - env.AcStore.AssertCalled(t, "DeleteExternalServiceRole", mock.Anything, - mock.MatchedBy(func(slug string) bool { return slug == extSvcSlug })) _, ok, _ := env.SkvStore.Get(context.Background(), tmpOrgID, extSvcSlug, kvStoreType) - require.False(t, ok, "secret should have been removed from store") + require.True(t, ok, "secret should have been kept in store") + }, + want: &extsvcauth.ExternalService{ + Name: extSvcSlug, + ID: extSvcSlug, + Secret: "not empty", }, - want: nil, wantErr: false, }, { name: "should remove service account when no permission", init: func(env *TestEnv) { // A previous service account was attached to this slug - env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, mock.Anything, mock.Anything).Return(extSvcAccID, nil) - env.SaSvc.On("DeleteServiceAccount", mock.Anything, mock.Anything, mock.Anything).Return(nil) - env.AcStore.On("DeleteExternalServiceRole", mock.Anything, mock.Anything).Return(nil) + env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, tmpOrgID, sa.ExtSvcPrefix+extSvcSlug). + Return(extSvcAccID, nil) + env.SaSvc.On("DeleteServiceAccount", mock.Anything, tmpOrgID, extSvcAccID).Return(nil) + env.AcStore.On("DeleteExternalServiceRole", mock.Anything, extSvcSlug).Return(nil) + // A token was previously stored in the secret store + _ = env.SkvStore.Set(context.Background(), tmpOrgID, extSvcSlug, kvStoreType, "ExtSvcSecretToken") }, cmd: extsvcauth.ExternalServiceRegistration{ Name: extSvcSlug, @@ -289,14 +271,8 @@ func TestExtSvcAccountsService_SaveExternalService(t *testing.T) { }, }, checks: func(t *testing.T, env *TestEnv) { - env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, - mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), - mock.MatchedBy(func(slug string) bool { return slug == sa.ExtSvcPrefix+extSvcSlug })) - env.SaSvc.AssertCalled(t, "DeleteServiceAccount", mock.Anything, - mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), - mock.MatchedBy(func(saID int64) bool { return saID == extSvcAccID })) - env.AcStore.AssertCalled(t, "DeleteExternalServiceRole", mock.Anything, - mock.MatchedBy(func(slug string) bool { return slug == extSvcSlug })) + _, ok, _ := env.SkvStore.Get(context.Background(), tmpOrgID, extSvcSlug, kvStoreType) + require.False(t, ok, "secret should have been removed from store") }, want: nil, wantErr: false, @@ -305,13 +281,26 @@ func TestExtSvcAccountsService_SaveExternalService(t *testing.T) { name: "should create new service account", init: func(env *TestEnv) { // No previous service account was attached to this slug - env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, mock.Anything, mock.Anything). + env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, tmpOrgID, sa.ExtSvcPrefix+extSvcSlug). Return(int64(0), sa.ErrServiceAccountNotFound.Errorf("mock")) - env.SaSvc.On("CreateServiceAccount", mock.Anything, mock.Anything, mock.Anything). + env.SaSvc.On("CreateServiceAccount", + mock.Anything, + tmpOrgID, + mock.MatchedBy(func(cmd *sa.CreateServiceAccountForm) bool { + return cmd.Name == sa.ExtSvcPrefix+extSvcSlug && *cmd.Role == roletype.RoleNone + })). Return(extSvcAccount, nil) + env.SaSvc.On("EnableServiceAccount", mock.Anything, extsvcauth.TmpOrgID, extSvcAccID, true).Return(nil) // Api Key was added without problem env.SaSvc.On("AddServiceAccountToken", mock.Anything, mock.Anything, mock.Anything).Return(&apikey.APIKey{}, nil) - env.AcStore.On("SaveExternalServiceRole", mock.Anything, mock.Anything).Return(nil) + env.AcStore.On("SaveExternalServiceRole", + mock.Anything, + mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool { + return cmd.ServiceAccountID == extSvcAccount.Id && cmd.ExternalServiceID == extSvcSlug && + cmd.OrgID == int64(ac.GlobalOrgID) && len(cmd.Permissions) == 1 && + cmd.Permissions[0] == extSvcPerms[0] + })). + Return(nil) }, cmd: extsvcauth.ExternalServiceRegistration{ Name: extSvcSlug, @@ -320,23 +309,6 @@ func TestExtSvcAccountsService_SaveExternalService(t *testing.T) { Permissions: extSvcPerms, }, }, - checks: func(t *testing.T, env *TestEnv) { - env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, - mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), - mock.MatchedBy(func(slug string) bool { return slug == sa.ExtSvcPrefix+extSvcSlug })) - env.SaSvc.AssertCalled(t, "CreateServiceAccount", mock.Anything, - mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), - mock.MatchedBy(func(cmd *sa.CreateServiceAccountForm) bool { - return cmd.Name == sa.ExtSvcPrefix+extSvcSlug && *cmd.Role == roletype.RoleNone - }), - ) - env.AcStore.AssertCalled(t, "SaveExternalServiceRole", mock.Anything, - mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool { - return cmd.ServiceAccountID == extSvcAccount.Id && cmd.ExternalServiceID == extSvcSlug && - cmd.OrgID == int64(ac.GlobalOrgID) && len(cmd.Permissions) == 1 && - cmd.Permissions[0] == extSvcPerms[0] - })) - }, want: &extsvcauth.ExternalService{ Name: extSvcSlug, ID: extSvcSlug, @@ -348,9 +320,17 @@ func TestExtSvcAccountsService_SaveExternalService(t *testing.T) { name: "should update service account", init: func(env *TestEnv) { // A previous service account was attached to this slug - env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, mock.Anything, mock.Anything). + env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, tmpOrgID, sa.ExtSvcPrefix+extSvcSlug). Return(int64(11), nil) - env.AcStore.On("SaveExternalServiceRole", mock.Anything, mock.Anything).Return(nil) + env.AcStore.On("SaveExternalServiceRole", + mock.Anything, + mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool { + return cmd.ServiceAccountID == int64(11) && cmd.ExternalServiceID == extSvcSlug && + cmd.OrgID == int64(ac.GlobalOrgID) && len(cmd.Permissions) == 1 && + cmd.Permissions[0] == extSvcPerms[0] + })). + Return(nil) + env.SaSvc.On("EnableServiceAccount", mock.Anything, extsvcauth.TmpOrgID, int64(11), true).Return(nil) // This time we don't add a token but rely on the secret store _ = env.SkvStore.Set(context.Background(), tmpOrgID, extSvcSlug, kvStoreType, "ExtSvcSecretToken") }, @@ -361,17 +341,6 @@ func TestExtSvcAccountsService_SaveExternalService(t *testing.T) { Permissions: extSvcPerms, }, }, - checks: func(t *testing.T, env *TestEnv) { - env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, - mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), - mock.MatchedBy(func(slug string) bool { return slug == sa.ExtSvcPrefix+extSvcSlug })) - env.AcStore.AssertCalled(t, "SaveExternalServiceRole", mock.Anything, - mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool { - return cmd.ServiceAccountID == int64(11) && cmd.ExternalServiceID == extSvcSlug && - cmd.OrgID == int64(ac.GlobalOrgID) && len(cmd.Permissions) == 1 && - cmd.Permissions[0] == extSvcPerms[0] - })) - }, want: &extsvcauth.ExternalService{ Name: extSvcSlug, ID: extSvcSlug, diff --git a/pkg/services/serviceaccounts/models.go b/pkg/services/serviceaccounts/models.go index 411b6f995b1..90b74ee6634 100644 --- a/pkg/services/serviceaccounts/models.go +++ b/pkg/services/serviceaccounts/models.go @@ -191,7 +191,7 @@ type ExtSvcAccount struct { type ManageExtSvcAccountCmd struct { ExtSvcSlug string - Enabled bool // disabled: the service account and its permissions will be deleted + Enabled bool OrgID int64 Permissions []accesscontrol.Permission } From 214535c1a9f0b531958870c08ebb215d26f6c934 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Fri, 27 Oct 2023 14:38:52 +0200 Subject: [PATCH 131/180] Alerting: Memoize labels suggestions calculation (#76972) --- .../components/rule-editor/LabelsField.tsx | 52 +++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx b/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx index 034b1e69f96..6e8b82d7a4b 100644 --- a/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx @@ -1,5 +1,4 @@ import { css, cx } from '@emotion/css'; -import { flattenDeep, compact } from 'lodash'; import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { FieldArrayMethodProps, useFieldArray, useFormContext } from 'react-hook-form'; @@ -18,7 +17,6 @@ import { LoadingPlaceholder, } from '@grafana/ui'; import { useDispatch } from 'app/types'; -import { RulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { fetchRulerRulesIfNotFetchedYet } from '../../state/actions'; @@ -30,7 +28,7 @@ interface Props { dataSourceName?: string | null; } -const useGetCustomLabels = (dataSourceName: string): { loading: boolean; labelsByKey: Record } => { +const useGetCustomLabels = (dataSourceName: string): { loading: boolean; labelsByKey: Record> } => { const dispatch = useDispatch(); useEffect(() => { @@ -38,33 +36,45 @@ const useGetCustomLabels = (dataSourceName: string): { loading: boolean; labelsB }, [dispatch, dataSourceName]); const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules); - const rulerRequest = rulerRuleRequests[dataSourceName]; - const result = rulerRequest?.result || {}; + const labelsByKeyResult = useMemo>>(() => { + const labelsByKey: Record> = {}; - //store all labels in a flat array and remove empty values - const labels = compact( - flattenDeep( - Object.keys(result).map((ruleGroupKey) => - result[ruleGroupKey].map((ruleItem: RulerRuleGroupDTO) => ruleItem.rules.map((item) => item.labels)) - ) - ) - ); + const rulerRulesConfig = rulerRequest?.result; + if (!rulerRulesConfig) { + return labelsByKey; + } - const labelsByKey: Record = {}; + const allRules = Object.values(rulerRulesConfig) + .flatMap((groups) => groups) + .flatMap((group) => group.rules); - labels.forEach((label: Record) => { - Object.entries(label).forEach(([key, value]) => { - labelsByKey[key] = [...new Set([...(labelsByKey[key] || []), value])]; + allRules.forEach((rule) => { + if (rule.labels) { + Object.entries(rule.labels).forEach(([key, value]) => { + if (!value) { + return; + } + + const labelEntry = labelsByKey[key]; + if (labelEntry) { + labelEntry.add(value); + } else { + labelsByKey[key] = new Set([value]); + } + }); + } }); - }); - return { loading: rulerRequest?.loading, labelsByKey }; + return labelsByKey; + }, [rulerRequest]); + + return { loading: rulerRequest?.loading, labelsByKey: labelsByKeyResult }; }; -function mapLabelsToOptions(items: string[] = []): Array> { - return items.map((item) => ({ label: item, value: item })); +function mapLabelsToOptions(items: Iterable = []): Array> { + return Array.from(items, (item) => ({ label: item, value: item })); } const RemoveButton: FC<{ From 83e9088314053bf208dbd1f2481334a41174a013 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Fri, 27 Oct 2023 14:45:04 +0200 Subject: [PATCH 132/180] AuthN: Set oauth client grant_types based on plugin state (#77248) * Disable plugin service account * Fix bug seen by linoman :100: Co-authored-by: linoman <2051016+linoman@users.noreply.github.com> * Account for PR feedback Co-authored-by: linoman <2051016+linoman@users.noreply.github.com> * Fix test data * Enable datasource plugins by default Co-authored-by: Andres Martinez Gotor * Update pkg/services/extsvcauth/oauthserver/oasimpl/service.go * Handle error differently * Fix service reg --------- Co-authored-by: linoman <2051016+linoman@users.noreply.github.com> Co-authored-by: Andres Martinez Gotor --- .../oauth-external-registration/plugin.json | 1 - pkg/plugins/plugindef/plugindef.cue | 3 - pkg/plugins/plugindef/plugindef_types_gen.go | 4 - pkg/services/authn/clients/ext_jwt_test.go | 2 +- pkg/services/extsvcauth/oauthserver/errors.go | 12 +- pkg/services/extsvcauth/oauthserver/models.go | 7 +- .../oasimpl/aggregate_store_test.go | 2 +- .../extsvcauth/oauthserver/oasimpl/service.go | 100 +++++++++++----- .../oauthserver/oasimpl/service_test.go | 112 +++++++++++++++++- .../oauthserver/oastest/store_mock.go | 23 +++- .../extsvcauth/oauthserver/store/database.go | 28 +++-- .../pluginsintegration/loader/loader_test.go | 3 +- .../serviceregistration.go | 6 +- 13 files changed, 226 insertions(+), 77 deletions(-) diff --git a/pkg/plugins/manager/testdata/oauth-external-registration/plugin.json b/pkg/plugins/manager/testdata/oauth-external-registration/plugin.json index c09e9ea5d0d..6530f2db5b7 100644 --- a/pkg/plugins/manager/testdata/oauth-external-registration/plugin.json +++ b/pkg/plugins/manager/testdata/oauth-external-registration/plugin.json @@ -19,7 +19,6 @@ }, "externalServiceRegistration": { "impersonation": { - "enabled" : true, "groups" : true, "permissions" : [ { diff --git a/pkg/plugins/plugindef/plugindef.cue b/pkg/plugins/plugindef/plugindef.cue index bd7026e6c93..8b35f4cb449 100644 --- a/pkg/plugins/plugindef/plugindef.cue +++ b/pkg/plugins/plugindef/plugindef.cue @@ -425,9 +425,6 @@ schemas: [{ } #Impersonation: { - // Enabled allows the service to request access tokens to impersonate users using the jwtbearer grant - // Defaults to true. - enabled?: bool // Groups allows the service to list the impersonated user's teams. // Defaults to true. groups?: bool diff --git a/pkg/plugins/plugindef/plugindef_types_gen.go b/pkg/plugins/plugindef/plugindef_types_gen.go index 20b04cebbf6..1213b6f4448 100644 --- a/pkg/plugins/plugindef/plugindef_types_gen.go +++ b/pkg/plugins/plugindef/plugindef_types_gen.go @@ -140,10 +140,6 @@ type Header struct { // Impersonation defines model for Impersonation. type Impersonation struct { - // Enabled allows the service to request access tokens to impersonate users using the jwtbearer grant - // Defaults to true. - Enabled *bool `json:"enabled,omitempty"` - // Groups allows the service to list the impersonated user's teams. // Defaults to true. Groups *bool `json:"groups,omitempty"` diff --git a/pkg/services/authn/clients/ext_jwt_test.go b/pkg/services/authn/clients/ext_jwt_test.go index 7f2f3e91f7d..5325baf3e3f 100644 --- a/pkg/services/authn/clients/ext_jwt_test.go +++ b/pkg/services/authn/clients/ext_jwt_test.go @@ -283,7 +283,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { Scopes: []string{"profile", "groups"}, }, initTestEnv: func(env *testEnv) { - env.oauthSvc.ExpectedErr = oauthserver.ErrClientNotFound("unknown-client-id") + env.oauthSvc.ExpectedErr = oauthserver.ErrClientNotFoundFn("unknown-client-id") }, orgID: 1, want: nil, diff --git a/pkg/services/extsvcauth/oauthserver/errors.go b/pkg/services/extsvcauth/oauthserver/errors.go index 43d395b4d49..942245b362e 100644 --- a/pkg/services/extsvcauth/oauthserver/errors.go +++ b/pkg/services/extsvcauth/oauthserver/errors.go @@ -1,8 +1,6 @@ package oauthserver import ( - "fmt" - "github.com/grafana/grafana/pkg/util/errutil" ) @@ -17,11 +15,11 @@ var ( ErrClientRequiredName = errutil.BadRequest( "oauthserver.required-client-name", errutil.WithPublicMessage("client name is required")).Errorf("Client name is required") + ErrClientNotFound = errutil.NotFound( + ErrClientNotFoundMessageID, + errutil.WithPublicMessage("Requested client has not been found")) ) -func ErrClientNotFound(clientID string) error { - return errutil.NotFound( - ErrClientNotFoundMessageID, - errutil.WithPublicMessage(fmt.Sprintf("Client '%s' not found", clientID))). - Errorf("client '%s' not found", clientID) +func ErrClientNotFoundFn(clientID string) error { + return ErrClientNotFound.Errorf("client '%s' not found", clientID) } diff --git a/pkg/services/extsvcauth/oauthserver/models.go b/pkg/services/extsvcauth/oauthserver/models.go index 606086895db..cae75077ffe 100644 --- a/pkg/services/extsvcauth/oauthserver/models.go +++ b/pkg/services/extsvcauth/oauthserver/models.go @@ -42,12 +42,13 @@ type OAuth2Server interface { HandleIntrospectionRequest(rw http.ResponseWriter, req *http.Request) } -//go:generate mockery --name Store --structname MockStore --outpkg oauthtest --filename store_mock.go --output ./oauthtest/ +//go:generate mockery --name Store --structname MockStore --outpkg oastest --filename store_mock.go --output ./oastest/ type Store interface { - RegisterExternalService(ctx context.Context, client *OAuthExternalService) error - SaveExternalService(ctx context.Context, client *OAuthExternalService) error GetExternalService(ctx context.Context, id string) (*OAuthExternalService, error) GetExternalServiceByName(ctx context.Context, name string) (*OAuthExternalService, error) GetExternalServicePublicKey(ctx context.Context, clientID string) (*jose.JSONWebKey, error) + RegisterExternalService(ctx context.Context, client *OAuthExternalService) error + SaveExternalService(ctx context.Context, client *OAuthExternalService) error + UpdateExternalServiceGrantTypes(ctx context.Context, clientID, grantTypes string) error } diff --git a/pkg/services/extsvcauth/oauthserver/oasimpl/aggregate_store_test.go b/pkg/services/extsvcauth/oauthserver/oasimpl/aggregate_store_test.go index b5899c87601..479e68e88a9 100644 --- a/pkg/services/extsvcauth/oauthserver/oasimpl/aggregate_store_test.go +++ b/pkg/services/extsvcauth/oauthserver/oasimpl/aggregate_store_test.go @@ -46,7 +46,7 @@ func TestOAuth2ServiceImpl_GetPublicKeyScopes(t *testing.T) { { name: "should error out when GetExternalService returns error", initTestEnv: func(env *TestEnv) { - env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFound("my-ext-service")) + env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFoundFn("my-ext-service")) }, wantErr: true, }, diff --git a/pkg/services/extsvcauth/oauthserver/oasimpl/service.go b/pkg/services/extsvcauth/oauthserver/oasimpl/service.go index 9aae5e8feae..250928ce9ac 100644 --- a/pkg/services/extsvcauth/oauthserver/oasimpl/service.go +++ b/pkg/services/extsvcauth/oauthserver/oasimpl/service.go @@ -22,6 +22,7 @@ import ( "golang.org/x/crypto/bcrypt" "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" @@ -33,12 +34,12 @@ import ( "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/store" "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/utils" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/signingkeys" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util/errutil" ) const ( @@ -61,7 +62,7 @@ type OAuth2ServiceImpl struct { publicKey any } -func ProvideService(router routing.RouteRegister, db db.DB, cfg *setting.Cfg, +func ProvideService(router routing.RouteRegister, bus bus.Bus, db db.DB, cfg *setting.Cfg, extSvcAccSvc serviceaccounts.ExtSvcAccountsService, accessControl ac.AccessControl, acSvc ac.Service, userSvc user.Service, teamSvc team.Service, keySvc signingkeys.Service, fmgmt *featuremgmt.FeatureManager) (*OAuth2ServiceImpl, error) { if !fmgmt.IsEnabled(featuremgmt.FlagExternalServiceAuth) { @@ -91,6 +92,8 @@ func ProvideService(router routing.RouteRegister, db db.DB, cfg *setting.Cfg, api := api.NewAPI(router, s) api.RegisterAPIEndpoints() + bus.AddEventListener(s.handlePluginStateChanged) + s.oauthProvider = newProvider(config, s, keySvc) return s, nil @@ -134,44 +137,50 @@ func (s *OAuth2ServiceImpl) GetExternalService(ctx context.Context, id string) ( return nil, err } - // Handle the case where the external service has no service account + if err := s.setClientUser(ctx, client); err != nil { + return nil, err + } + + s.cache.Set(id, *client, cacheExpirationTime) + return client, nil +} + +// setClientUser sets the SignedInUser and SelfPermissions fields of the client +func (s *OAuth2ServiceImpl) setClientUser(ctx context.Context, client *oauthserver.OAuthExternalService) error { if client.ServiceAccountID == oauthserver.NoServiceAccountID { - s.logger.Debug("GetExternalService: service has no service account, hence no permission", "id", id, "name", client.Name) - // Create a signed in user with no role and no permissions + s.logger.Debug("GetExternalService: service has no service account, hence no permission", "client_id", client.ClientID, "name", client.Name) + + // Create a signed in user with no role and no permission client.SignedInUser = &user.SignedInUser{ UserID: oauthserver.NoServiceAccountID, OrgID: oauthserver.TmpOrgID, Name: client.Name, Permissions: map[int64]map[string][]string{oauthserver.TmpOrgID: {}}, } - s.cache.Set(id, *client, cacheExpirationTime) - return client, nil + return nil } - // Retrieve self permissions and generate a signed in user - s.logger.Debug("GetExternalService: fetch permissions", "client id", id) + s.logger.Debug("GetExternalService: fetch permissions", "client_id", client.ClientID) sa, err := s.saService.RetrieveExtSvcAccount(ctx, oauthserver.TmpOrgID, client.ServiceAccountID) if err != nil { - s.logger.Error("GetExternalService: error fetching service account", "id", id, "error", err) - return nil, err + s.logger.Error("GetExternalService: error fetching service account", "id", client.ClientID, "error", err) + return err } client.SignedInUser = &user.SignedInUser{ UserID: sa.ID, OrgID: oauthserver.TmpOrgID, - OrgRole: sa.Role, // Need this to compute the permissions in OSS + OrgRole: sa.Role, Login: sa.Login, Name: sa.Name, Permissions: map[int64]map[string][]string{}, } client.SelfPermissions, err = s.acService.GetUserPermissions(ctx, client.SignedInUser, ac.Options{}) if err != nil { - s.logger.Error("GetExternalService: error fetching permissions", "id", id, "error", err) - return nil, err + s.logger.Error("GetExternalService: error fetching permissions", "client_id", client.ClientID, "error", err) + return err } client.SignedInUser.Permissions[oauthserver.TmpOrgID] = ac.GroupScopesByAction(client.SelfPermissions) - - s.cache.Set(id, *client, cacheExpirationTime) - return client, nil + return nil } // SaveExternalService creates or updates an external service in the database, it generates client_id and secrets and @@ -186,14 +195,9 @@ func (s *OAuth2ServiceImpl) SaveExternalService(ctx context.Context, registratio // Check if the client already exists in store client, errFetchExtSvc := s.sqlstore.GetExternalServiceByName(ctx, registration.Name) - if errFetchExtSvc != nil { - var srcError errutil.Error - if errors.As(errFetchExtSvc, &srcError) { - if srcError.MessageID != oauthserver.ErrClientNotFoundMessageID { - s.logger.Error("Error fetching service", "external service", registration.Name, "error", errFetchExtSvc) - return nil, errFetchExtSvc - } - } + if errFetchExtSvc != nil && !errors.Is(errFetchExtSvc, oauthserver.ErrClientNotFound) { + s.logger.Error("Error fetching service", "external service", registration.Name, "error", errFetchExtSvc) + return nil, errFetchExtSvc } // Otherwise, create a new client if client == nil { @@ -386,13 +390,10 @@ func (s *OAuth2ServiceImpl) handleKeyOptions(ctx context.Context, keyOption *ext // handleRegistrationPermissions parses the registration form to retrieve requested permissions and adds default // permissions when impersonation is requested func (*OAuth2ServiceImpl) handleRegistrationPermissions(registration *extsvcauth.ExternalServiceRegistration) ([]ac.Permission, []ac.Permission) { - selfPermissions := []ac.Permission{} + selfPermissions := registration.Self.Permissions impersonatePermissions := []ac.Permission{} - if registration.Self.Enabled { - selfPermissions = append(selfPermissions, registration.Self.Permissions...) - } - if registration.Impersonation.Enabled { + if len(registration.Impersonation.Permissions) > 0 { requiredForToken := []ac.Permission{ {Action: ac.ActionUsersRead, Scope: oauthserver.ScopeGlobalUsersSelf}, {Action: ac.ActionUsersPermissionsRead, Scope: oauthserver.ScopeUsersSelf}, @@ -405,3 +406,42 @@ func (*OAuth2ServiceImpl) handleRegistrationPermissions(registration *extsvcauth } return selfPermissions, impersonatePermissions } + +// handlePluginStateChanged reset the client authorized grant_types according to the plugin state +func (s *OAuth2ServiceImpl) handlePluginStateChanged(ctx context.Context, event *pluginsettings.PluginStateChangedEvent) error { + s.logger.Info("Plugin state changed", "pluginId", event.PluginId, "enabled", event.Enabled) + + // Retrieve client associated to the plugin + slug := slugify.Slugify(event.PluginId) + client, err := s.sqlstore.GetExternalServiceByName(ctx, slug) + if err != nil { + if errors.Is(err, oauthserver.ErrClientNotFound) { + s.logger.Debug("No external service linked to this plugin", "pluginId", event.PluginId) + return nil + } + s.logger.Error("Error fetching service", "pluginId", event.PluginId, "error", err) + return err + } + + // Since we will change the grants, clear cache entry + s.cache.Delete(client.ClientID) + + if !event.Enabled { + // Plugin is disabled => remove all grant_types + return s.sqlstore.UpdateExternalServiceGrantTypes(ctx, client.ClientID, "") + } + + if err := s.setClientUser(ctx, client); err != nil { + return err + } + + // The plugin has self permissions (not only impersonate) + canOnlyImpersonate := len(client.SelfPermissions) == 1 && (client.SelfPermissions[0].Action == ac.ActionUsersImpersonate) + selfEnabled := len(client.SelfPermissions) > 0 && !canOnlyImpersonate + // The plugin declared impersonate permissions + impersonateEnabled := len(client.ImpersonatePermissions) > 0 + + grantTypes := s.computeGrantTypes(selfEnabled, impersonateEnabled) + + return s.sqlstore.UpdateExternalServiceGrantTypes(ctx, client.ClientID, strings.Join(grantTypes, ",")) +} diff --git a/pkg/services/extsvcauth/oauthserver/oasimpl/service_test.go b/pkg/services/extsvcauth/oauthserver/oasimpl/service_test.go index afcedd06cc2..761de64a997 100644 --- a/pkg/services/extsvcauth/oauthserver/oasimpl/service_test.go +++ b/pkg/services/extsvcauth/oauthserver/oasimpl/service_test.go @@ -24,6 +24,8 @@ import ( "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/oastest" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" sa "github.com/grafana/grafana/pkg/services/serviceaccounts" saTests "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" "github.com/grafana/grafana/pkg/services/signingkeys/signingkeystest" @@ -114,7 +116,7 @@ func TestOAuth2ServiceImpl_SaveExternalService(t *testing.T) { name: "should create a new client without permissions", init: func(env *TestEnv) { // No client at the beginning - env.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFound(serviceName)) + env.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFoundFn(serviceName)) env.OAuthStore.On("SaveExternalService", mock.Anything, mock.Anything).Return(nil) // Return a service account ID @@ -139,7 +141,7 @@ func TestOAuth2ServiceImpl_SaveExternalService(t *testing.T) { name: "should allow client credentials grant with correct permissions", init: func(env *TestEnv) { // No client at the beginning - env.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFound(serviceName)) + env.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFoundFn(serviceName)) env.OAuthStore.On("SaveExternalService", mock.Anything, mock.Anything).Return(nil) // Return a service account ID @@ -175,7 +177,7 @@ func TestOAuth2ServiceImpl_SaveExternalService(t *testing.T) { name: "should allow jwt bearer grant and set default permissions", init: func(env *TestEnv) { // No client at the beginning - env.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFound(serviceName)) + env.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFoundFn(serviceName)) env.OAuthStore.On("SaveExternalService", mock.Anything, mock.Anything).Return(nil) // The service account needs to be created with a permission to impersonate users env.SAService.On("ManageExtSvcAccount", mock.Anything, mock.Anything).Return(int64(10), nil) @@ -310,7 +312,7 @@ func TestOAuth2ServiceImpl_GetExternalService(t *testing.T) { { name: "should return error when the client was not found", init: func(env *TestEnv) { - env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFound(serviceName)) + env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFoundFn(serviceName)) }, wantErr: true, }, @@ -474,3 +476,105 @@ mgGaC8vUIigFQVsVB+v/HZ4yG1Rcvysig+tyNk1dZQpozpFc2dGmzHlGhw== require.True(t, result.Generated) }) } + +func TestOAuth2ServiceImpl_handlePluginStateChanged(t *testing.T) { + pluginID := "my-app" + clientID := "RANDOMID" + impersonatePermission := []ac.Permission{{Action: ac.ActionUsersImpersonate, Scope: ac.ScopeUsersAll}} + selfPermission := append(impersonatePermission, ac.Permission{Action: ac.ActionUsersRead, Scope: ac.ScopeUsersAll}) + saID := int64(101) + client := &oauthserver.OAuthExternalService{ + ID: 11, + Name: pluginID, + ClientID: clientID, + Secret: "SECRET", + ServiceAccountID: saID, + } + clientWithImpersonate := &oauthserver.OAuthExternalService{ + ID: 11, + Name: pluginID, + ClientID: clientID, + Secret: "SECRET", + ImpersonatePermissions: []ac.Permission{ + {Action: ac.ActionUsersRead, Scope: ac.ScopeUsersAll}, + }, + ServiceAccountID: saID, + } + extSvcAcc := &sa.ExtSvcAccount{ + ID: saID, + Login: "sa-my-app", + Name: pluginID, + OrgID: extsvcauth.TmpOrgID, + IsDisabled: false, + Role: org.RoleNone, + } + + tests := []struct { + name string + init func(*TestEnv) + cmd *pluginsettings.PluginStateChangedEvent + }{ + { + name: "should do nothing with not found", + init: func(te *TestEnv) { + te.OAuthStore.On("GetExternalServiceByName", mock.Anything, "unknown").Return(nil, oauthserver.ErrClientNotFoundFn("unknown")) + }, + cmd: &pluginsettings.PluginStateChangedEvent{PluginId: "unknown", OrgId: 1, Enabled: false}, + }, + { + name: "should remove grants", + init: func(te *TestEnv) { + te.OAuthStore.On("GetExternalServiceByName", mock.Anything, pluginID).Return(clientWithImpersonate, nil) + te.OAuthStore.On("UpdateExternalServiceGrantTypes", mock.Anything, clientID, "").Return(nil) + }, + cmd: &pluginsettings.PluginStateChangedEvent{PluginId: pluginID, OrgId: 1, Enabled: false}, + }, + { + name: "should set both grants", + init: func(te *TestEnv) { + te.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(clientWithImpersonate, nil) + te.SAService.On("RetrieveExtSvcAccount", mock.Anything, extsvcauth.TmpOrgID, saID).Return(extSvcAcc, nil) + te.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything, mock.Anything).Return(selfPermission, nil) + te.OAuthStore.On("UpdateExternalServiceGrantTypes", mock.Anything, clientID, + string(fosite.GrantTypeClientCredentials)+","+string(fosite.GrantTypeJWTBearer)).Return(nil) + }, + cmd: &pluginsettings.PluginStateChangedEvent{PluginId: pluginID, OrgId: 1, Enabled: true}, + }, + { + name: "should set impersonate grant", + init: func(te *TestEnv) { + te.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(clientWithImpersonate, nil) + te.SAService.On("RetrieveExtSvcAccount", mock.Anything, extsvcauth.TmpOrgID, saID).Return(extSvcAcc, nil) + te.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything, mock.Anything).Return(impersonatePermission, nil) + te.OAuthStore.On("UpdateExternalServiceGrantTypes", mock.Anything, clientID, string(fosite.GrantTypeJWTBearer)).Return(nil) + }, + cmd: &pluginsettings.PluginStateChangedEvent{PluginId: pluginID, OrgId: 1, Enabled: true}, + }, + { + name: "should set client_credentials grant", + init: func(te *TestEnv) { + te.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(client, nil) + te.SAService.On("RetrieveExtSvcAccount", mock.Anything, extsvcauth.TmpOrgID, saID).Return(extSvcAcc, nil) + te.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything, mock.Anything).Return(selfPermission, nil) + te.OAuthStore.On("UpdateExternalServiceGrantTypes", mock.Anything, clientID, string(fosite.GrantTypeClientCredentials)).Return(nil) + }, + cmd: &pluginsettings.PluginStateChangedEvent{PluginId: pluginID, OrgId: 1, Enabled: true}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + env := setupTestEnv(t) + if tt.init != nil { + tt.init(env) + } + + err := env.S.handlePluginStateChanged(context.Background(), tt.cmd) + require.NoError(t, err) + + // Check that mocks were called as expected + env.OAuthStore.AssertExpectations(t) + env.SAService.AssertExpectations(t) + env.AcStore.AssertExpectations(t) + }) + } +} diff --git a/pkg/services/extsvcauth/oauthserver/oastest/store_mock.go b/pkg/services/extsvcauth/oauthserver/oastest/store_mock.go index 30cf07882b9..17a705e3e33 100644 --- a/pkg/services/extsvcauth/oauthserver/oastest/store_mock.go +++ b/pkg/services/extsvcauth/oauthserver/oastest/store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. +// Code generated by mockery v2.35.2. DO NOT EDIT. package oastest @@ -122,13 +122,26 @@ func (_m *MockStore) SaveExternalService(ctx context.Context, client *oauthserve return r0 } -type mockConstructorTestingTNewMockStore interface { - mock.TestingT - Cleanup(func()) +// UpdateExternalServiceGrantTypes provides a mock function with given fields: ctx, clientID, grantTypes +func (_m *MockStore) UpdateExternalServiceGrantTypes(ctx context.Context, clientID string, grantTypes string) error { + ret := _m.Called(ctx, clientID, grantTypes) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, clientID, grantTypes) + } else { + r0 = ret.Error(0) + } + + return r0 } // NewMockStore creates a new instance of MockStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewMockStore(t mockConstructorTestingTNewMockStore) *MockStore { +// The first argument is typically a *testing.T value. +func NewMockStore(t interface { + mock.TestingT + Cleanup(func()) +}) *MockStore { mock := &MockStore{} mock.Mock.Test(t) diff --git a/pkg/services/extsvcauth/oauthserver/store/database.go b/pkg/services/extsvcauth/oauthserver/store/database.go index 124fc5888a6..c12c29e25cb 100644 --- a/pkg/services/extsvcauth/oauthserver/store/database.go +++ b/pkg/services/extsvcauth/oauthserver/store/database.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/utils" - "github.com/grafana/grafana/pkg/util/errutil" ) type store struct { @@ -101,13 +100,8 @@ func (s *store) SaveExternalService(ctx context.Context, client *oauthserver.OAu } return s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { previous, errFetchExtSvc := getExternalServiceByName(sess, client.Name) - if errFetchExtSvc != nil { - var srcError errutil.Error - if errors.As(errFetchExtSvc, &srcError) { - if srcError.MessageID != oauthserver.ErrClientNotFoundMessageID { - return errFetchExtSvc - } - } + if errFetchExtSvc != nil && !errors.Is(errFetchExtSvc, oauthserver.ErrClientNotFound) { + return errFetchExtSvc } if previous == nil { return registerExternalService(sess, client) @@ -132,7 +126,7 @@ func (s *store) GetExternalService(ctx context.Context, id string) (*oauthserver return err } if !found { - return oauthserver.ErrClientNotFound(id) + return oauthserver.ErrClientNotFoundFn(id) } impersonatePermQuery := `SELECT action, scope FROM oauth_impersonate_permission WHERE client_id = ?` @@ -157,7 +151,7 @@ func (s *store) GetExternalServicePublicKey(ctx context.Context, clientID string return err } if !found { - return oauthserver.ErrClientNotFound(clientID) + return oauthserver.ErrClientNotFoundFn(clientID) } return nil }); err != nil { @@ -209,7 +203,7 @@ func getExternalServiceByName(sess *db.Session, name string) (*oauthserver.OAuth return nil, err } if !found { - return nil, oauthserver.ErrClientNotFound(name) + return nil, oauthserver.ErrClientNotFoundFn(name) } impersonatePermQuery := `SELECT action, scope FROM oauth_impersonate_permission WHERE client_id = ?` @@ -217,3 +211,15 @@ func getExternalServiceByName(sess *db.Session, name string) (*oauthserver.OAuth return res, errPerm } + +func (s *store) UpdateExternalServiceGrantTypes(ctx context.Context, clientID, grantTypes string) error { + if clientID == "" { + return oauthserver.ErrClientRequiredID + } + + return s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { + query := `UPDATE oauth_client SET grant_types = ? WHERE client_id = ?` + _, err := sess.Exec(query, grantTypes, clientID) + return err + }) +} diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index 553ccbc4e5d..5878f93c924 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -530,8 +530,7 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) { }, ExternalServiceRegistration: &plugindef.ExternalServiceRegistration{ Impersonation: &plugindef.Impersonation{ - Enabled: boolPtr(true), - Groups: boolPtr(true), + Groups: boolPtr(true), Permissions: []plugindef.Permission{ { Action: "read", diff --git a/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go b/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go index 722891c111e..662b8160fd6 100644 --- a/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go +++ b/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go @@ -41,11 +41,7 @@ func (s *Service) RegisterExternalService(ctx context.Context, svcName string, p impersonation := extsvcauth.ImpersonationCfg{} if svc.Impersonation != nil { impersonation.Permissions = toAccessControlPermissions(svc.Impersonation.Permissions) - if svc.Impersonation.Enabled != nil { - impersonation.Enabled = *svc.Impersonation.Enabled - } else { - impersonation.Enabled = true - } + impersonation.Enabled = enabled if svc.Impersonation.Groups != nil { impersonation.Groups = *svc.Impersonation.Groups } else { From 09e496acfd0f63d3ac09b25067991bf2af343a81 Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Fri, 27 Oct 2023 07:19:43 -0600 Subject: [PATCH 133/180] Explore TraceView: Remove 'Scroll to top' button (#77158) * Remove button from Explore but keep in plugins * Remove topOfViewRef from trace view container --- public/app/features/explore/Explore.tsx | 1 - .../app/features/explore/TraceView/TraceView.tsx | 4 ++-- .../explore/TraceView/TraceViewContainer.test.tsx | 11 ++--------- .../explore/TraceView/TraceViewContainer.tsx | 8 ++------ .../VirtualizedTraceView.test.tsx | 4 ++-- .../TraceTimelineViewer/VirtualizedTraceView.tsx | 14 ++++++++------ 6 files changed, 16 insertions(+), 26 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index c41cbde0c0c..4121b2f8d2c 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -501,7 +501,6 @@ export class Explore extends React.PureComponent { splitOpenFn={this.onSplitOpen('traceView')} scrollElement={this.scrollElement} queryResponse={queryResponse} - topOfViewRef={this.topOfViewRef} /> ) diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx index bbe96d1b37c..1b5e8d29414 100644 --- a/public/app/features/explore/TraceView/TraceView.tsx +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -64,8 +64,8 @@ type Props = { traceProp: Trace; queryResponse: PanelData; datasource: DataSourceApi | undefined; - topOfViewRef: RefObject; - topOfViewRefType: TopOfViewRefType; + topOfViewRef?: RefObject; + topOfViewRefType?: TopOfViewRefType; createSpanLink?: SpanLinkFunc; }; diff --git a/public/app/features/explore/TraceView/TraceViewContainer.test.tsx b/public/app/features/explore/TraceView/TraceViewContainer.test.tsx index da155cec531..b9d5cf46755 100644 --- a/public/app/features/explore/TraceView/TraceViewContainer.test.tsx +++ b/public/app/features/explore/TraceView/TraceViewContainer.test.tsx @@ -1,6 +1,6 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import React, { createRef } from 'react'; +import React from 'react'; import { Provider } from 'react-redux'; import { getDefaultTimeRange, LoadingState } from '@grafana/data'; @@ -24,17 +24,10 @@ function renderTraceViewContainer(frames = [frameOld]) { series: [], timeRange: getDefaultTimeRange(), }; - const topOfViewRef = createRef(); const { container, baseElement } = render( - {}} - queryResponse={mockPanelData} - topOfViewRef={topOfViewRef} - /> + {}} queryResponse={mockPanelData} /> ); return { diff --git a/public/app/features/explore/TraceView/TraceViewContainer.tsx b/public/app/features/explore/TraceView/TraceViewContainer.tsx index 2f37c70d31b..42e4d1fa1de 100644 --- a/public/app/features/explore/TraceView/TraceViewContainer.tsx +++ b/public/app/features/explore/TraceView/TraceViewContainer.tsx @@ -1,11 +1,10 @@ -import React, { RefObject, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { DataFrame, SplitOpen, PanelData } from '@grafana/data'; import { PanelChrome } from '@grafana/ui/src/components/PanelChrome/PanelChrome'; import { StoreState, useSelector } from 'app/types'; import { TraceView } from './TraceView'; -import { TopOfViewRefType } from './components/TraceTimelineViewer/VirtualizedTraceView'; import { transformDataFrames } from './utils/transform'; interface Props { @@ -14,13 +13,12 @@ interface Props { exploreId: string; scrollElement?: Element; queryResponse: PanelData; - topOfViewRef: RefObject; } export function TraceViewContainer(props: Props) { // At this point we only show single trace const frame = props.dataFrames[0]; - const { dataFrames, splitOpenFn, exploreId, scrollElement, topOfViewRef, queryResponse } = props; + const { dataFrames, splitOpenFn, exploreId, scrollElement, queryResponse } = props; const traceProp = useMemo(() => transformDataFrames(frame), [frame]); const datasource = useSelector( (state: StoreState) => state.explore.panes[props.exploreId]?.datasourceInstance ?? undefined @@ -40,8 +38,6 @@ export function TraceViewContainer(props: Props) { traceProp={traceProp} queryResponse={queryResponse} datasource={datasource} - topOfViewRef={topOfViewRef} - topOfViewRefType={TopOfViewRefType.Explore} /> ); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.test.tsx index 2ceddbfb0c6..cf81e80bbd4 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.test.tsx @@ -25,7 +25,7 @@ import VirtualizedTraceView, { VirtualizedTraceViewProps } from './VirtualizedTr jest.mock('./SpanTreeOffset'); const trace = transformTraceData(traceGenerator.trace({ numberOfSpans: 2 }))!; -const topOfExploreViewRef = jest.fn(); + let props = { childrenHiddenIDs: new Set(), childrenToggle: jest.fn(), @@ -41,7 +41,7 @@ let props = { spanNameColumnWidth: 0.5, trace, uiFind: 'uiFind', - topOfExploreViewRef, + topOfViewRef: jest.fn(), } as unknown as VirtualizedTraceViewProps; describe('', () => { diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx index 1c7af44fe83..392075699ea 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx @@ -566,12 +566,14 @@ export class UnthemedVirtualizedTraceView extends React.Component - + {this.props.topOfViewRef && ( + + )} ); } From f750c3194eabcca24f21dc8018dcced91ff24bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Toulet?= <35176601+AgnesToulet@users.noreply.github.com> Date: Fri, 27 Oct 2023 15:28:41 +0200 Subject: [PATCH 134/180] Chore: Fix bingo variables for Windows (#73830) * Chore: Fix bingo variables for Windows * make the change for Windows-only --- .bingo/Variables.mk | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.bingo/Variables.mk b/.bingo/Variables.mk index 84f3fe5ce8c..2c9eb124234 100644 --- a/.bingo/Variables.mk +++ b/.bingo/Variables.mk @@ -2,7 +2,12 @@ # All tools are designed to be build inside $GOBIN. BINGO_DIR := $(dir $(lastword $(MAKEFILE_LIST))) GOPATH ?= $(shell go env GOPATH) -GOBIN ?= $(firstword $(subst :, ,${GOPATH}))/bin +ifeq ($(OS),Windows_NT) + PATHSEP := $(if $(COMSPEC),;,:) + GOBIN ?= $(firstword $(subst $(PATHSEP), ,$(subst \,/,${GOPATH})))/bin +else + GOBIN ?= $(firstword $(subst :, ,${GOPATH}))/bin +endif GO ?= $(shell which go) # Below generated variables ensure that every time a tool under each variable is invoked, the correct version From bf554d121cc116457f3432f2448314381b234016 Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Fri, 27 Oct 2023 14:37:23 +0100 Subject: [PATCH 135/180] Explore: Avoid reinitializing graph on every query run (#77281) --- .../app/features/explore/Graph/useStructureRev.test.ts | 9 +++++++++ public/app/features/explore/Graph/useStructureRev.ts | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/public/app/features/explore/Graph/useStructureRev.test.ts b/public/app/features/explore/Graph/useStructureRev.test.ts index dd808005929..650e1ac0a72 100644 --- a/public/app/features/explore/Graph/useStructureRev.test.ts +++ b/public/app/features/explore/Graph/useStructureRev.test.ts @@ -51,6 +51,15 @@ beforeAll(() => { describe('useStructureRev', () => { afterEach(() => resetCounters()); + // mirrors the logic in componentDidUpdate in packages/grafana-ui/src/components/GraphNG/GraphNG.tsx, + // which treats all falsy values for structureRev as a signal to reconfig the graph + it('should start from a thruthy value', () => { + let frames: DataFrame[] = [toDataFrame({ fields: [{ name: 'time', type: FieldType.time, values: [1, 2, 3] }] })]; + const { result } = renderHook((frames) => useStructureRev(frames), { initialProps: frames }); + + expect(result.current).not.toBeFalsy(); + }); + it('should increment only when relevant fields in frame change', () => { let frames: DataFrame[] = [toDataFrame({ fields: [{ name: 'time', type: FieldType.time, values: [1, 2, 3] }] })]; const { result, rerender } = renderHook((frames) => useStructureRev(frames), { initialProps: frames }); diff --git a/public/app/features/explore/Graph/useStructureRev.ts b/public/app/features/explore/Graph/useStructureRev.ts index 23490e4d170..cb51571a42c 100644 --- a/public/app/features/explore/Graph/useStructureRev.ts +++ b/public/app/features/explore/Graph/useStructureRev.ts @@ -4,7 +4,7 @@ import { useCounter, usePrevious } from 'react-use'; import { DataFrame, compareArrayValues, compareDataFrameStructures } from '@grafana/data'; export function useStructureRev(frames: DataFrame[]) { - const [structureRev, { inc }] = useCounter(0); + const [structureRev, { inc }] = useCounter(1); const previousFrames = usePrevious(frames); // We need to increment structureRev when the number of series changes. From 9b472b37267e3a58015aa0543b4b40635822771a Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 27 Oct 2023 06:59:49 -0700 Subject: [PATCH 136/180] K8s: Use client-go to test legacy playlist changes (#77245) --- pkg/tests/apis/helper.go | 143 ++++++++++++++++--- pkg/tests/apis/playlist/playlist_test.go | 169 +++++++++++++++++++++-- pkg/tests/apis/types.go | 24 ---- 3 files changed, 280 insertions(+), 56 deletions(-) diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index 86d0b5666cf..52fc57bd574 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -7,12 +7,20 @@ import ( "fmt" "io" "net/http" + "os" "testing" "github.com/stretchr/testify/require" + "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" "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/yaml" + "k8s.io/apimachinery/pkg/runtime/serializer/yaml" + yamlutil "k8s.io/apimachinery/pkg/util/yaml" + + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/server" @@ -40,9 +48,6 @@ type K8sTestHelper struct { // // Registered groups groups []metav1.APIGroup - - // Used to build the URL paths - selectedGVR schema.GroupVersionResource } func NewK8sTestHelper(t *testing.T) *K8sTestHelper { @@ -67,7 +72,7 @@ func NewK8sTestHelper(t *testing.T) *K8sTestHelper { c.Org2 = c.createTestUsers(int64(2)) // Read the API groups - rsp := doRequest(c, RequestParams{ + rsp := DoRequest(c, RequestParams{ User: c.Org1.Viewer, Path: "/apis", // Accept: "application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json", @@ -76,6 +81,68 @@ func NewK8sTestHelper(t *testing.T) *K8sTestHelper { return c } +type ResourceClientArgs struct { + User User + Namespace string + GVR schema.GroupVersionResource +} + +type K8sResourceClient struct { + t *testing.T + Args ResourceClientArgs + Resource dynamic.ResourceInterface +} + +// This will set the expected Group/Version/Resource and return the discovery info if found +func (c *K8sTestHelper) GetResourceClient(args ResourceClientArgs) *K8sResourceClient { + c.t.Helper() + + if args.Namespace == "" { + args.Namespace = c.namespacer(args.User.Identity.GetOrgID()) + } + + return &K8sResourceClient{ + t: c.t, + Args: args, + Resource: args.User.Client.Resource(args.GVR).Namespace(args.Namespace), + } +} + +// Cast the error to status error +func (c *K8sTestHelper) AsStatusError(err error) *errors.StatusError { + c.t.Helper() + + if err == nil { + return nil + } + + //nolint:errorlint + statusError, ok := err.(*errors.StatusError) + require.True(c.t, ok) + return statusError +} + +// remove the meta keys that are expected to change each time +func (c *K8sResourceClient) SanitizeJSON(v *unstructured.Unstructured) string { + c.t.Helper() + + copy := v.DeepCopy().Object + meta, ok := copy["metadata"].(map[string]any) + require.True(c.t, ok) + + replaceMeta := []string{"creationTimestamp", "resourceVersion", "uid"} + for _, key := range replaceMeta { + old, ok := meta[key] + require.True(c.t, ok) + require.NotEmpty(c.t, old) + meta[key] = fmt.Sprintf("${%s}", key) + } + + out, err := json.MarshalIndent(copy, "", " ") + require.NoError(c.t, err) + return string(out) +} + type OrgUsers struct { Admin User Editor User @@ -84,6 +151,7 @@ type OrgUsers struct { type User struct { Identity identity.Requester + Client *dynamic.DynamicClient password string } @@ -106,13 +174,6 @@ type K8sResponse[T any] struct { type AnyResourceResponse = K8sResponse[AnyResource] type AnyResourceListResponse = K8sResponse[AnyResourceList] -// This will set the expected Group/Version/Resource and return the discovery info if found -func (c *K8sTestHelper) SetGroupVersionResource(gvr schema.GroupVersionResource) { - c.t.Helper() - - c.selectedGVR = gvr -} - func (c *K8sTestHelper) PostResource(user User, resource string, payload AnyResource) AnyResourceResponse { c.t.Helper() @@ -130,7 +191,7 @@ func (c *K8sTestHelper) PostResource(user User, resource string, payload AnyReso body, err := json.Marshal(payload) require.NoError(c.t, err) - return doRequest(c, RequestParams{ + return DoRequest(c, RequestParams{ Method: http.MethodPost, Path: path, User: user, @@ -147,7 +208,7 @@ func (c *K8sTestHelper) PutResource(user User, resource string, payload AnyResou body, err := json.Marshal(payload) require.NoError(c.t, err) - return doRequest(c, RequestParams{ + return DoRequest(c, RequestParams{ Method: http.MethodPut, Path: path, User: user, @@ -155,20 +216,20 @@ func (c *K8sTestHelper) PutResource(user User, resource string, payload AnyResou }, &AnyResource{}) } -func (c *K8sTestHelper) List(user User, namespace string) AnyResourceListResponse { +func (c *K8sTestHelper) List(user User, namespace string, gvr schema.GroupVersionResource) AnyResourceListResponse { c.t.Helper() - return doRequest(c, RequestParams{ + return DoRequest(c, RequestParams{ User: user, Path: fmt.Sprintf("/apis/%s/%s/namespaces/%s/%s", - c.selectedGVR.Group, - c.selectedGVR.Version, + gvr.Group, + gvr.Version, namespace, - c.selectedGVR.Resource), + gvr.Resource), }, &AnyResourceList{}) } -func doRequest[T any](c *K8sTestHelper, params RequestParams, result *T) K8sResponse[T] { +func DoRequest[T any](c *K8sTestHelper, params RequestParams, result *T) K8sResponse[T] { c.t.Helper() if params.Method == "" { @@ -224,12 +285,38 @@ func doRequest[T any](c *K8sTestHelper, params RequestParams, result *T) K8sResp r.Status = s r.Result = nil } - } else { - _ = yaml.Unmarshal(r.Body, r.Result) } return r } +// Read local JSON or YAML file into a resource +func (c *K8sTestHelper) LoadYAMLOrJSONFile(fpath string) *unstructured.Unstructured { + c.t.Helper() + + //nolint:gosec + raw, err := os.ReadFile(fpath) + require.NoError(c.t, err) + require.NotEmpty(c.t, raw) + return c.LoadYAMLOrJSON(string(raw)) +} + +// Read local JSON or YAML file into a resource +func (c *K8sTestHelper) LoadYAMLOrJSON(body string) *unstructured.Unstructured { + c.t.Helper() + + decoder := yamlutil.NewYAMLOrJSONDecoder(bytes.NewReader([]byte(body)), 100) + var rawObj runtime.RawExtension + err := decoder.Decode(&rawObj) + require.NoError(c.t, err) + + obj, _, err := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme).Decode(rawObj.Raw, nil, nil) + require.NoError(c.t, err) + unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + require.NoError(c.t, err) + + return &unstructured.Unstructured{Object: unstructuredMap} +} + func (c K8sTestHelper) createTestUsers(orgId int64) OrgUsers { c.t.Helper() @@ -252,6 +339,7 @@ func (c K8sTestHelper) createTestUsers(orgId int64) OrgUsers { supportbundlestest.NewFakeBundleService()) require.NoError(c.t, err) + baseUrl := fmt.Sprintf("http://%s", c.env.Server.HTTPServer.Listener.Addr()) createUser := func(key string, role org.RoleType) User { u, err := userSvc.Create(context.Background(), &user.CreateUserCommand{ DefaultOrgRole: string(role), @@ -272,8 +360,19 @@ func (c K8sTestHelper) createTestUsers(orgId int64) OrgUsers { require.NoError(c.t, err) require.Equal(c.t, orgId, s.OrgID) require.Equal(c.t, role, s.OrgRole) // make sure the role was set properly + + config := &rest.Config{ + Host: baseUrl, + Username: s.Login, + Password: key, + } + + client, err := dynamic.NewForConfig(config) + require.NoError(c.t, err) + return User{ Identity: s, + Client: client, password: key, } } diff --git a/pkg/tests/apis/playlist/playlist_test.go b/pkg/tests/apis/playlist/playlist_test.go index 053f01a634b..9d475d82d5a 100644 --- a/pkg/tests/apis/playlist/playlist_test.go +++ b/pkg/tests/apis/playlist/playlist_test.go @@ -1,12 +1,16 @@ package playlist import ( + "context" + "net/http" + "strings" "testing" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/tests/apis" ) @@ -15,31 +19,176 @@ func TestPlaylist(t *testing.T) { t.Skip("skipping integration test") } helper := apis.NewK8sTestHelper(t) - helper.SetGroupVersionResource( - schema.GroupVersionResource{ - Group: "playlist.grafana.app", - Version: "v0alpha1", - Resource: "playlists", - }) + gvr := schema.GroupVersionResource{ + Group: "playlist.grafana.app", + Version: "v0alpha1", + Resource: "playlists", + } - t.Run("Check List from different org users", func(t *testing.T) { + t.Run("Check direct List permissions from different org users", func(t *testing.T) { // Check view permissions - rsp := helper.List(helper.Org1.Viewer, "default") + rsp := helper.List(helper.Org1.Viewer, "default", gvr) require.Equal(t, 200, rsp.Response.StatusCode) require.NotNil(t, rsp.Result) require.Empty(t, rsp.Result.Items) require.Nil(t, rsp.Status) // Check view permissions - rsp = helper.List(helper.Org2.Viewer, "default") + rsp = helper.List(helper.Org2.Viewer, "default", gvr) require.Equal(t, 403, rsp.Response.StatusCode) // Org2 can not see default namespace require.Nil(t, rsp.Result) require.Equal(t, metav1.StatusReasonForbidden, rsp.Status.Reason) // Check view permissions - rsp = helper.List(helper.Org2.Viewer, "org-22") + rsp = helper.List(helper.Org2.Viewer, "org-22", gvr) require.Equal(t, 403, rsp.Response.StatusCode) // Unknown/not a member require.Nil(t, rsp.Result) require.Equal(t, metav1.StatusReasonForbidden, rsp.Status.Reason) }) + + t.Run("Check k8s client-go List from different org users", func(t *testing.T) { + // Check Org1 Viewer + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Viewer, + Namespace: "", // << fills in the value org1 is allowed to see! + GVR: gvr, + }) + rsp, err := client.Resource.List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Empty(t, rsp.Items) + + // Check org2 viewer can not see org1 (default namespace) + client = helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org2.Viewer, + Namespace: "default", // actually org1 + GVR: gvr, + }) + rsp, err = client.Resource.List(context.Background(), metav1.ListOptions{}) + statusError := helper.AsStatusError(err) + require.Nil(t, rsp) + require.Equal(t, metav1.StatusReasonForbidden, statusError.Status().Reason) + + // Check invalid namespace + client = helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org2.Viewer, + Namespace: "org-22", // org 22 does not exist + GVR: gvr, + }) + rsp, err = client.Resource.List(context.Background(), metav1.ListOptions{}) + statusError = helper.AsStatusError(err) + require.Nil(t, rsp) + require.Equal(t, metav1.StatusReasonForbidden, statusError.Status().Reason) + }) + + t.Run("Check playlist CRUD in legacy API appears in k8s apis", func(t *testing.T) { + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Editor, + GVR: gvr, + }) + + // This includes the raw dashboard values that are currently sent (but should not be and are ignored) + legacyPayload := `{ + "name": "Test", + "interval": "20s", + "items": [ + { + "type": "dashboard_by_uid", + "value": "xCmMwXdVz", + "dashboards": [ + { + "name": "The dashboard", + "kind": "dashboard", + "uid": "xCmMwXdVz", + "url": "/d/xCmMwXdVz/barchart-label-rotation-and-skipping", + "tags": ["barchart", "gdev", "graph-ng", "panel-tests"], + "location": "d1de6240-fd2e-4e13-99b6-f9d0c6b0550d" + } + ] + }, + { + "type": "dashboard_by_tag", + "value": "graph-ng", + "dashboards": [ "..." ] + } + ], + "uid": "" + }` + legacyCreate := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodPost, + Path: "/api/playlists", + Body: []byte(legacyPayload), + }, &playlist.Playlist{}) + require.NotNil(t, legacyCreate.Result) + uid := legacyCreate.Result.UID + require.NotEmpty(t, uid) + + expectedResult := `{ + "apiVersion": "playlist.grafana.app/v0alpha1", + "kind": "Playlist", + "metadata": { + "creationTimestamp": "${creationTimestamp}", + "name": "` + uid + `", + "namespace": "default", + "resourceVersion": "${resourceVersion}", + "uid": "${uid}" + }, + "spec": { + "title": "Test", + "interval": "20s", + "items": [ + { + "type": "dashboard_by_uid", + "value": "xCmMwXdVz" + }, + { + "type": "dashboard_by_tag", + "value": "graph-ng" + } + ] + } + }` + + // List includes the expected result + k8sList, err := client.Resource.List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Equal(t, 1, len(k8sList.Items)) + require.JSONEq(t, expectedResult, client.SanitizeJSON(&k8sList.Items[0])) + + // Get should return the same result + found, err := client.Resource.Get(context.Background(), uid, metav1.GetOptions{}) + require.NoError(t, err) + require.JSONEq(t, expectedResult, client.SanitizeJSON(found)) + + // Now modify the interval + updatedInterval := `"interval": "10m"` + legacyPayload = strings.Replace(legacyPayload, `"interval": "20s"`, updatedInterval, 1) + expectedResult = strings.Replace(expectedResult, `"interval": "20s"`, updatedInterval, 1) + dtoResponse := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodPut, + Path: "/api/playlists/" + uid, + Body: []byte(legacyPayload), + }, &playlist.PlaylistDTO{}) + require.Equal(t, uid, dtoResponse.Result.Uid) + require.Equal(t, "10m", dtoResponse.Result.Interval) + + // Make sure the changed interval is now returned from k8s + found, err = client.Resource.Get(context.Background(), uid, metav1.GetOptions{}) + require.NoError(t, err) + require.JSONEq(t, expectedResult, client.SanitizeJSON(found)) + + // Delete does not return anything + _ = apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodDelete, + Path: "/api/playlists/" + uid, + Body: []byte(legacyPayload), + }, &playlist.PlaylistDTO{}) // response is empty + + found, err = client.Resource.Get(context.Background(), uid, metav1.GetOptions{}) + statusError := helper.AsStatusError(err) + require.Nil(t, found) + require.Equal(t, metav1.StatusReasonNotFound, statusError.Status().Reason) + }) } diff --git a/pkg/tests/apis/types.go b/pkg/tests/apis/types.go index b405de90cd7..5c8a24563c2 100644 --- a/pkg/tests/apis/types.go +++ b/pkg/tests/apis/types.go @@ -1,12 +1,7 @@ package apis import ( - "encoding/json" - "os" - - "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/yaml" ) type AnyResource struct { @@ -24,22 +19,3 @@ type AnyResourceList struct { Items []map[string]any `json:"items,omitempty"` } - -// Read local JSON or YAML file into a resource -func (c *K8sTestHelper) LoadAnyResource(fpath string) AnyResource { - c.t.Helper() - - //nolint:gosec - raw, err := os.ReadFile(fpath) - require.NoError(c.t, err) - require.NotEmpty(c.t, raw) - - res := &AnyResource{} - if json.Valid(raw) { - err = json.Unmarshal(raw, res) - } else { - err = yaml.Unmarshal(raw, res) - } - require.NoError(c.t, err) - return *res -} From dba846fe547a14a6faad4ad9cb901eaa3ee6fb24 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 27 Oct 2023 15:09:23 +0100 Subject: [PATCH 137/180] Chore: Some renovate config tidy up (#77275) * update some comments, remove some things from the ignore list * remove @mdx-js/react since storybook now directly depends on it * add issue link to comment * exclude @locker from grouped patch updates * ignore grafana-e2e from renovate --- .github/renovate.json5 | 23 +++++++++++------------ packages/grafana-ui/package.json | 1 - yarn.lock | 10 ---------- 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 6f64cde93fd..9000449aab6 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -4,28 +4,20 @@ ], "enabledManagers": ["npm"], "ignoreDeps": [ - "commander", // we are planning to remove this, so no need to update it - "execa", // we should bump this once we move to esm modules - "history", // we should bump this together with react-router-dom - "@mdx-js/react", // storybook peer-depends on its 1.x version, we should upgrade this when we upgrade storybook + "history", // we should bump this together with react-router-dom (see https://github.com/grafana/grafana/issues/76744) + "react-router-dom", // we should bump this together with history (see https://github.com/grafana/grafana/issues/76744) "monaco-editor", // due to us exposing this via @grafana/ui/CodeEditor's props bumping can break plugins "react-hook-form", // due to us exposing these hooks via @grafana/ui form components bumping can break plugins "react-redux", // react-beautiful-dnd depends on react-redux 7.x, we need to update that one first - "react-router-dom", // we should bump this together with history - "ts-loader", // we should remove ts-loader and use babel-loader instead - "ora", // we should bump this once we move to esm modules - "@locker/near-membrane-dom", // critical library. we need to bump this only intentionally - "@locker/near-membrane-shared", // critical library. we need to bump this only intentionally - "@locker/near-membrane-shared-dom", // critical library. we need to bump this only intentionally ], "includePaths": ["package.json", "packages/**", "public/app/plugins/**"], - "ignorePaths": ["emails/**", "plugins-bundled/**", "**/mocks/**"], + "ignorePaths": ["emails/**", "plugins-bundled/**", "**/mocks/**", "packages/grafana-e2e/**"], "labels": ["area/frontend", "dependencies", "no-backport", "no-changelog"], "postUpdateOptions": ["yarnDedupeHighest"], "packageRules": [ { "matchUpdateTypes": ["patch"], - "excludePackagePatterns": ["^@?storybook"], + "excludePackagePatterns": ["^@?storybook", "^@locker"], "extends": ["schedule:monthly"], "groupName": "Monthly patch updates" }, @@ -78,6 +70,13 @@ ], "reviewers": ["leeoniya"], }, + { + "groupName": "locker", + "matchPackagePrefixes": [ + "@locker/" + ] + "reviewers": ["team:grafana/plugins-platform-frontend"], + }, ], "pin": { "enabled": false diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index d04401940d5..5db1d39b1bb 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -117,7 +117,6 @@ "devDependencies": { "@babel/core": "7.23.2", "@grafana/tsconfig": "^1.2.0-rc1", - "@mdx-js/react": "1.6.22", "@rollup/plugin-node-resolve": "15.2.3", "@storybook/addon-a11y": "7.4.5", "@storybook/addon-actions": "7.4.5", diff --git a/yarn.lock b/yarn.lock index 8c65fabf306..1e3a9293d2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3369,7 +3369,6 @@ __metadata: "@grafana/schema": 10.3.0-pre "@grafana/tsconfig": ^1.2.0-rc1 "@leeoniya/ufuzzy": 1.0.8 - "@mdx-js/react": 1.6.22 "@monaco-editor/react": 4.6.0 "@popperjs/core": 2.11.8 "@react-aria/button": 3.8.0 @@ -4213,15 +4212,6 @@ __metadata: languageName: node linkType: hard -"@mdx-js/react@npm:1.6.22": - version: 1.6.22 - resolution: "@mdx-js/react@npm:1.6.22" - peerDependencies: - react: ^16.13.1 || ^17.0.0 - checksum: bc84bd514bc127f898819a0c6f1a6915d9541011bd8aefa1fcc1c9bea8939f31051409e546bdec92babfa5b56092a16d05ef6d318304ac029299df5181dc94c8 - languageName: node - linkType: hard - "@mdx-js/react@npm:^2.1.5": version: 2.3.0 resolution: "@mdx-js/react@npm:2.3.0" From 8effa165dd86be581d1ad4f68485d091f3610a13 Mon Sep 17 00:00:00 2001 From: Ihor Yeromin Date: Fri, 27 Oct 2023 17:22:48 +0300 Subject: [PATCH 138/180] Tooltip: New styles (#76964) --- .betterer.results | 3 -- .../src/components/VizTooltip/HeaderLabel.tsx | 53 +++++++++++++++++-- .../VizTooltip/VizTooltipContent.tsx | 2 +- .../VizTooltip/VizTooltipFooter.tsx | 7 +-- .../VizTooltip/VizTooltipHeader.tsx | 2 +- .../components/uPlot/plugins/CloseButton.tsx | 14 ++--- .../uPlot/plugins/TooltipPlugin2.tsx | 4 +- 7 files changed, 61 insertions(+), 24 deletions(-) diff --git a/.betterer.results b/.betterer.results index 196906a6074..658ec507d17 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1025,9 +1025,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], - "packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "packages/grafana-ui/src/components/uPlot/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/packages/grafana-ui/src/components/VizTooltip/HeaderLabel.tsx b/packages/grafana-ui/src/components/VizTooltip/HeaderLabel.tsx index 9303073f222..a7341409a15 100644 --- a/packages/grafana-ui/src/components/VizTooltip/HeaderLabel.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/HeaderLabel.tsx @@ -1,4 +1,4 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; @@ -7,6 +7,7 @@ import { HorizontalGroup, Tooltip } from '..'; import { useStyles2 } from '../../themes'; import { LabelValue } from './types'; +import { getColorIndicatorClass } from './utils'; interface Props { headerLabel: LabelValue; @@ -15,12 +16,20 @@ interface Props { export const HeaderLabel = ({ headerLabel }: Props) => { const styles = useStyles2(getStyles); + const { label, value, color, colorIndicator } = headerLabel; + return (
- {headerLabel.label} - - {headerLabel.value} + {label} + {color && ( + + )} + + {value}
@@ -28,18 +37,35 @@ export const HeaderLabel = ({ headerLabel }: Props) => { }; const getStyles = (theme: GrafanaTheme2) => ({ + hgContainer: css({ + flexGrow: 1, + }), + colorIndicator: css({ + marginRight: theme.spacing(0.5), + }), label: css({ color: theme.colors.text.secondary, paddingRight: theme.spacing(0.5), fontWeight: 400, }), value: css({ + width: '12px', + height: '12px', + borderRadius: theme.shape.radius.default, + }), + series: css({ + width: '14px', + height: '4px', + borderRadius: theme.shape.radius.pill, + }), + labelValue: css({ fontWeight: 500, lineHeight: '18px', alignSelf: 'center', }), wrapper: css({ display: 'flex', + alignItems: 'center', flexDirection: 'row', textOverflow: 'ellipsis', overflow: 'hidden', @@ -47,4 +73,23 @@ const getStyles = (theme: GrafanaTheme2) => ({ width: '250px', maskImage: 'linear-gradient(90deg, rgba(0, 0, 0, 1) 80%, transparent)', }), + hexagon: css({}), + pie_1_4: css({}), + pie_2_4: css({}), + pie_3_4: css({}), + marker_sm: css({ + width: '4px', + height: '4px', + borderRadius: theme.shape.radius.circle, + }), + marker_md: css({ + width: '8px', + height: '8px', + borderRadius: theme.shape.radius.circle, + }), + marker_lg: css({ + width: '12px', + height: '12px', + borderRadius: theme.shape.radius.circle, + }), }); diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx index 6cedcfa9744..5df6a2f58da 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx @@ -39,7 +39,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ flex: 1, gap: 4, borderTop: `1px solid ${theme.colors.border.medium}`, - padding: `${theme.spacing(1)} 0`, + padding: theme.spacing(1), }), customContentPadding: css({ padding: `${theme.spacing(1)} 0`, diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx index 490d62bf582..9bbbf862b50 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx @@ -49,17 +49,18 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: 'flex', flexDirection: 'column', flex: 1, + padding: theme.spacing(0), }), dataLinks: css({ - height: 40, overflowX: 'auto', overflowY: 'hidden', whiteSpace: 'nowrap', - borderTop: `1px solid ${theme.colors.border.medium}`, maskImage: 'linear-gradient(90deg, rgba(0, 0, 0, 1) 80%, transparent)', + borderTop: `1px solid ${theme.colors.border.medium}`, + padding: theme.spacing(1), }), addAnnotations: css({ borderTop: `1px solid ${theme.colors.border.medium}`, - paddingTop: theme.spacing(1), + padding: theme.spacing(1), }), }); diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeader.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeader.tsx index fe7233f551d..0cfb21a8981 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeader.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeader.tsx @@ -37,6 +37,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: 'flex', flexDirection: 'column', flex: 1, - paddingBottom: theme.spacing(1), + padding: theme.spacing(1), }), }); diff --git a/packages/grafana-ui/src/components/uPlot/plugins/CloseButton.tsx b/packages/grafana-ui/src/components/uPlot/plugins/CloseButton.tsx index e1ddfa2dd89..6fab4b71c21 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/CloseButton.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/CloseButton.tsx @@ -16,20 +16,14 @@ type Props = { export const CloseButton = ({ onClick, 'aria-label': ariaLabel, style }: Props) => { const styles = useStyles2(getStyles); return ( - + ); }; const getStyles = (theme: GrafanaTheme2) => css({ position: 'absolute', - right: theme.spacing(0.5), - top: theme.spacing(1), + margin: '0px', + right: theme.spacing(1), + top: theme.spacing(1.25), }); diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index 9699817e6c1..51b3a1cda48 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -128,6 +128,7 @@ export const TooltipPlugin2 = ({ config, render }: TooltipPlugin2Props) => { // in some ways this is similar to ClickOutsideWrapper.tsx const downEventOutside = (e: Event) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions let isOutside = (e.target as HTMLDivElement).closest(`.${styles.tooltipWrapper}`) !== domRef.current; if (isOutside) { @@ -284,7 +285,7 @@ export const TooltipPlugin2 = ({ config, render }: TooltipPlugin2Props) => { if (plot && isHovering) { return createPortal(
- {isPinned && } + {isPinned && } {contents}
, plot.over @@ -299,7 +300,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ top: 0, left: 0, zIndex: theme.zIndex.tooltip, - padding: '8px', whiteSpace: 'pre', borderRadius: theme.shape.radius.default, position: 'absolute', From ff67b03dc8a45b2cd522612d8b982a30754b7740 Mon Sep 17 00:00:00 2001 From: Isabel <76437239+imatwawana@users.noreply.github.com> Date: Fri, 27 Oct 2023 10:28:47 -0400 Subject: [PATCH 139/180] Added panel actions menu content (#77162) * Added panel actions menu content * Formatted text as heading * Added links, formatted bullet list items, and general copy edits * Fixed Vale and Prettier warnings * Remove the word "actions" * Fixed explanation * Updated description of Extensions * Apply suggestion from review Co-authored-by: David Harris * Updated intro text for panel menu * Apply suggestions from code review * Fixed linting issues --------- Co-authored-by: David Harris --- .../panel-editor-overview/index.md | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/docs/sources/panels-visualizations/panel-editor-overview/index.md b/docs/sources/panels-visualizations/panel-editor-overview/index.md index 3da60773c34..83f3257cfa2 100644 --- a/docs/sources/panels-visualizations/panel-editor-overview/index.md +++ b/docs/sources/panels-visualizations/panel-editor-overview/index.md @@ -33,10 +33,32 @@ In the panel editor, you can update all the elements of a visualization includin ![Panel editor](/media/docs/grafana/panels-visualizations/screenshot-panel-editor-view.png) -To add a panel in a new dashboard click **+ Add visualization** in the middle of the dashboard. To add a panel to an existing dashboard, click **Add** in the dashboard header and select **Visualization** in the dropdown: +To add a panel in a new dashboard click **+ Add visualization** in the middle of the dashboard. To add a panel to an existing dashboard, click **Add** in the dashboard header and select **Visualization** in the drop-down: ![Add dropdown](/media/docs/grafana/dashboards/screenshot-add-dropdown-10.0.png) +## Panel menu + +To access the panel editor, hover over the top-right corner of any panel. Click the panel menu icon that appears and select **Edit**. The panel menu gives you access to the following actions: + +- **View**: View the panel in full screen. +- **Edit**: Open the panel editor to edit panel and visualization options. +- **Share**: Share the panel as a link, embed, or library panel. +- **Explore**: Open the panel in **Explore**, where you can focus on your query. +- **Inspect**: Open the **Inspect** drawer, where you can review the panel data, stats, metadata, JSON, and query. + - **Data**: Open the **Inspect** drawer in the **Data** tab. + - **Query**: Open the **Inspect** drawer in the **Query** tab. + - **Panel JSON**: Open the **Inspect** drawer in the **JSON** tab. +- **Extensions**: Access other actions provided by installed applications, such as declaring an incident. Note that this option doesn't appear unless you have app plugins installed which contribute an [extension](https://grafana.com/developers/plugin-tools/ui-extensions/) to the panel menu. +- **More**: Access other panel actions. + - **Duplicate**: Make a copy of the panel. Duplicated panels query data separately from the original panel. You can use the special `Dashboard` data source to [share the same query results across panels][] instead. + - **Copy**: Copy the panel to the clipboard. + - **Create library panel**: Create a panel that can be imported into other dashboards. + - **Create alert**: Open the alert rule configuration page in **Alerting**, where you can [create a Grafana-managed alert] based on the panel queries. + - **Hide legend**: Hide the panel legend. + - **Get help**: Send a snapshot or panel data to Grafana Labs Technical Support. +- **Remove**: Remove the panel from the dashboard. + ## Panel editor This section describes the areas of the Grafana panel editor. @@ -45,18 +67,18 @@ This section describes the areas of the Grafana panel editor. - **Discard:** Discards changes you have made to the panel since you last saved the dashboard. - **Save:** Saves changes you made to the panel. - - **Apply:** Applies changes you made and closes the panel editor, returning you to the dashboard. You will have to save the dashboard to persist the applied changes. + - **Apply:** Applies changes you made and closes the panel editor, returning you to the dashboard. You'll have to save the dashboard to persist the applied changes. 1. Visualization preview: The visualization preview section contains the following options: - - **Table view:** Convert any visualization to a table so you can see the data. Table views are helpful for troubleshooting. This view only contains the raw data. It does not include transformations you might have applied to the data or the formatting options available in the [Table][] visualization. + - **Table view:** Convert any visualization to a table so you can see the data. Table views are helpful for troubleshooting. This view only contains the raw data. It doesn't include transformations you might have applied to the data or the formatting options available in the [Table][] visualization. - **Fill:** The visualization preview fills the available space. If you change the width of the side pane or height of the bottom pane the visualization changes to fill the available space. - - **Actual:** The visualization preview will have the exact size as the size on the dashboard. If not enough space is available, the visualization will scale down preserving the aspect ratio. + - **Actual:** The visualization preview has the exact size as the size on the dashboard. If not enough space is available, the visualization scales down preserving the aspect ratio. - **Time range controls:** **Default** is either the browser local timezone or the timezone selected at a higher level. 1. Data section: The data section contains tabs where you enter queries, transform your data, and create alert rules (if applicable). - - **Query tab:** Select your data source and enter queries here. For more information, refer to [Add a query][]. When you create a new dashboard, you'll be prompted to select a data source before you get to the panel editor. You set or update the data source in existing dashboards using the dropdown in the **Query** tab. + - **Query tab:** Select your data source and enter queries here. For more information, refer to [Add a query][]. When you create a new dashboard, you'll be prompted to select a data source before you get to the panel editor. You set or update the data source in existing dashboards using the drop-down in the **Query** tab. - **Transform tab:** Apply data transformations. For more information, refer to [Transform data][]. - **Alert tab:** Write alert rules. For more information, refer to [the overview of Grafana Alerting][]. @@ -69,14 +91,14 @@ The inspect drawer helps you understand and troubleshoot your panels. You can vi To access the panel inspect drawer from the edit view, hover over any part of the panel to display the actions menu on the top right corner. Click the menu and select **Inspect**. {{% admonition type="note" %}} -Not all panel types include all tabs. For example, dashboard list panels do not have raw data to inspect, so they do not display the Stats, Data, or Query tabs. +Not all panel types include all tabs. For example, dashboard list panels don't have raw data to inspect, so they don't display the Stats, Data, or Query tabs. {{% /admonition %}} The panel inspector consists of the following options: 1. The panel inspect drawer displays opens a drawer on the right side. Click the arrow in the upper right corner to expand or reduce the drawer pane. -1. **Data tab -** Shows the raw data returned by the query with transformations applied. Field options such as overrides and value mappings are not applied by default. +1. **Data tab -** Shows the raw data returned by the query with transformations applied. Field options such as overrides and value mappings aren't applied by default. 1. **Stats tab -** Shows how long your query takes and how much it returns. @@ -98,4 +120,10 @@ The panel inspector consists of the following options: [the overview of Grafana Alerting]: "/docs/grafana/ -> /docs/grafana//alerting" [the overview of Grafana Alerting]: "/docs/grafana-cloud/ -> /docs/grafana//alerting" + +[share the same query results across panels]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/query-transform-data/share-query" +[share the same query results across panels]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/share-query" + +[create a Grafana-managed alert]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-grafana-managed-rule/#create-alerts-from-panels" +[create a Grafana-managed alert]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-grafana-managed-rule#create-alerts-from-panels" {{% /docs/reference %}} From 046791e2be937031c0da44dfdef7c95d29274ee2 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Fri, 27 Oct 2023 17:17:19 +0200 Subject: [PATCH 140/180] InfluxDB: Response parser improvements (#76852) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove retention policy lookup * Back to one big function * %10 less memory allocation pkg: github.com/grafana/grafana/pkg/tsdb/influxdb/influxql │ 1.txt │ 2.txt │ │ sec/op │ sec/op vs base │ ParseBigJson-10 540.9m ± 3% 474.0m ± 2% -12.37% (p=0.000 n=10) │ 1.txt │ 2.txt │ │ B/op │ B/op vs base │ ParseBigJson-10 580.6Mi ± 0% 573.2Mi ± 0% -1.28% (p=0.000 n=10) │ 1.txt │ 2.txt │ │ allocs/op │ allocs/op vs base │ ParseBigJson-10 10.123M ± 0% 9.086M ± 0% -10.25% (p=0.000 n=10) * Slightly better results comparing with the previous commit pkg: github.com/grafana/grafana/pkg/tsdb/influxdb/influxql │ 2.txt │ 3.txt │ │ sec/op │ sec/op vs base │ ParseBigJson-10 474.0m ± 1% 503.4m ± 3% +6.21% (p=0.000 n=10) │ 2.txt │ 3.txt │ │ B/op │ B/op vs base │ ParseBigJson-10 573.2Mi ± 0% 564.0Mi ± 0% -1.60% (p=0.000 n=10) │ 2.txt │ 3.txt │ │ allocs/op │ allocs/op vs base │ ParseBigJson-10 9.086M ± 0% 9.052M ± 0% -0.37% (p=0.000 n=10) * Split into smaller functions * Unit test for parseTimestamp --- pkg/tsdb/influxdb/influxql/response_parser.go | 88 +++++++------------ .../influxdb/influxql/response_parser_test.go | 26 +++++- 2 files changed, 59 insertions(+), 55 deletions(-) diff --git a/pkg/tsdb/influxdb/influxql/response_parser.go b/pkg/tsdb/influxdb/influxql/response_parser.go index 22972d209ca..bd09676cb8f 100644 --- a/pkg/tsdb/influxdb/influxql/response_parser.go +++ b/pkg/tsdb/influxdb/influxql/response_parser.go @@ -16,7 +16,16 @@ import ( ) var ( + timeColumn = "time" + timeColumnName = "Time" + valueColumnName = "Value" + legendFormat = regexp.MustCompile(`\[\[([\@\/\w-]+)(\.[\@\/\w-]+)*\]\]*|\$([\@\w-]+?)*`) + + timeArray []time.Time + floatArray []*float64 + stringArray []*string + boolArray []*bool ) const ( @@ -66,35 +75,34 @@ func parseJSON(buf io.Reader) (models.Response, error) { } func transformRows(rows []models.Row, query models.Query) data.Frames { - // pre-allocate frames - this can save many allocations - cols := 0 + // Create a map for faster column name lookups + columnToLowerCase := make(map[string]string) for _, row := range rows { - cols += len(row.Columns) + for _, column := range row.Columns { + columnToLowerCase[column] = strings.ToLower(column) + } } - frames := make([]*data.Frame, 0, len(rows)+cols) + + // Preallocate for the worst-case scenario + frames := make([]*data.Frame, 0, len(rows)*len(rows[0].Columns)) // frameName is pre-allocated. So we can reuse it, saving memory. // It's sized for a reasonably-large name, but will grow if needed. frameName := make([]byte, 0, 128) - retentionPolicyQuery := isRetentionPolicyQuery(query) - tagValuesQuery := isTagValuesQuery(query) - for _, row := range rows { var hasTimeCol = false - for _, column := range row.Columns { - if strings.ToLower(column) == "time" { - hasTimeCol = true - } + if _, ok := columnToLowerCase[timeColumn]; ok { + hasTimeCol = true } if !hasTimeCol { - newFrame := newFrameWithoutTimeField(row, retentionPolicyQuery, tagValuesQuery) + newFrame := newFrameWithoutTimeField(row, query) frames = append(frames, newFrame) } else { for colIndex, column := range row.Columns { - if column == "time" { + if columnToLowerCase[column] == timeColumn { continue } newFrame := newFrameWithTimeField(row, column, colIndex, query, frameName) @@ -107,20 +115,21 @@ func transformRows(rows []models.Row, query models.Query) data.Frames { } func newFrameWithTimeField(row models.Row, column string, colIndex int, query models.Query, frameName []byte) *data.Frame { - var timeArray []time.Time - var floatArray []*float64 - var stringArray []*string - var boolArray []*bool + timeArray = timeArray[:0] + floatArray = floatArray[:0] + stringArray = stringArray[:0] + boolArray = boolArray[:0] + valType := typeof(row.Values, colIndex) for _, valuePair := range row.Values { timestamp, timestampErr := parseTimestamp(valuePair[0]) - // we only add this row if the timestamp is valid if timestampErr != nil { continue } timeArray = append(timeArray, timestamp) + switch valType { case "string": value, ok := valuePair[colIndex].(string) @@ -144,19 +153,19 @@ func newFrameWithTimeField(row models.Row, column string, colIndex int, query mo } } - timeField := data.NewField("Time", nil, timeArray) + timeField := data.NewField(timeColumnName, nil, timeArray) var valueField *data.Field switch valType { case "string": - valueField = data.NewField("Value", row.Tags, stringArray) + valueField = data.NewField(valueColumnName, row.Tags, stringArray) case "json.Number": - valueField = data.NewField("Value", row.Tags, floatArray) + valueField = data.NewField(valueColumnName, row.Tags, floatArray) case "bool": - valueField = data.NewField("Value", row.Tags, boolArray) + valueField = data.NewField(valueColumnName, row.Tags, boolArray) case "null": - valueField = data.NewField("Value", row.Tags, floatArray) + valueField = data.NewField(valueColumnName, row.Tags, floatArray) } name := string(formatFrameName(row, column, query, frameName[:])) @@ -164,35 +173,14 @@ func newFrameWithTimeField(row models.Row, column string, colIndex int, query mo return newDataFrame(name, query.RawQuery, timeField, valueField, getVisType(query.ResultFormat)) } -func newFrameWithoutTimeField(row models.Row, retentionPolicyQuery bool, tagValuesQuery bool) *data.Frame { +func newFrameWithoutTimeField(row models.Row, query models.Query) *data.Frame { var values []string - if retentionPolicyQuery { - values = make([]string, 1, len(row.Values)) - } else { - values = make([]string, 0, len(row.Values)) - } - for _, valuePair := range row.Values { - if tagValuesQuery { + if strings.Contains(strings.ToLower(query.RawQuery), strings.ToLower("SHOW TAG VALUES")) { if len(valuePair) >= 2 { values = append(values, valuePair[1].(string)) } - } else if retentionPolicyQuery { - // We want to know whether the given retention policy is the default one or not. - // If it is default policy then we should add it to the beginning. - // The index 4 gives us if that policy is default or not. - // https://docs.influxdata.com/influxdb/v1.8/query_language/explore-schema/#show-retention-policies - // Only difference is v0.9. In that version we don't receive shardGroupDuration value. - // https://archive.docs.influxdata.com/influxdb/v0.9/query_language/schema_exploration/#show-retention-policies - // Since it is always the last value we will check that last value always. - if len(valuePair) >= 1 { - if valuePair[len(row.Columns)-1].(bool) { - values[0] = valuePair[0].(string) - } else { - values = append(values, valuePair[0].(string)) - } - } } else { if len(valuePair) >= 1 { values = append(values, valuePair[0].(string)) @@ -342,11 +330,3 @@ func getVisType(resFormat string) data.VisType { return graphVisType } } - -func isTagValuesQuery(query models.Query) bool { - return strings.Contains(strings.ToLower(query.RawQuery), strings.ToLower("SHOW TAG VALUES")) -} - -func isRetentionPolicyQuery(query models.Query) bool { - return strings.Contains(strings.ToLower(query.RawQuery), strings.ToLower("SHOW RETENTION POLICIES")) -} diff --git a/pkg/tsdb/influxdb/influxql/response_parser_test.go b/pkg/tsdb/influxdb/influxql/response_parser_test.go index b089542f8eb..b84eeb1860f 100644 --- a/pkg/tsdb/influxdb/influxql/response_parser_test.go +++ b/pkg/tsdb/influxdb/influxql/response_parser_test.go @@ -739,7 +739,7 @@ func TestResponseParser_Parse_RetentionPolicy(t *testing.T) { query := models.Query{RefID: "metricFindQuery", RawQuery: "SHOW RETENTION POLICIES"} policyFrame := data.NewFrame("", data.NewField("Value", nil, []string{ - "bar", "autogen", "5m_avg", "1m_avg", + "autogen", "bar", "5m_avg", "1m_avg", }), ) @@ -871,3 +871,27 @@ func TestResponseParser_Parse(t *testing.T) { }) } } + +func TestParseTimestamp(t *testing.T) { + validValue := json.Number("1609459200000") // Milliseconds since epoch (January 1, 2021) + invalidValue := "invalid" + + t.Run("ValidTimestamp", func(t *testing.T) { + parsedTime, err := parseTimestamp(validValue) + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + expectedTime := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) + if !parsedTime.Equal(expectedTime) { + t.Errorf("Expected time: %v, got: %v", expectedTime, parsedTime) + } + }) + + t.Run("InvalidTimestamp", func(t *testing.T) { + _, err := parseTimestamp(invalidValue) + if err == nil { + t.Errorf("Expected an error, got nil") + } + }) +} From 470d879c804f4abe2fd969098548e685a6a82c26 Mon Sep 17 00:00:00 2001 From: Kyle Cunningham Date: Fri, 27 Oct 2023 10:30:31 -0500 Subject: [PATCH 141/180] Transformations: Move debug to drawer (#76281) * Move debug to drawer * Prettier * Remove render actions arg * Remove unused import --- .../TransformationEditor.tsx | 44 ++++++++++--------- .../TransformationOperationRow.tsx | 9 ++-- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationEditor.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationEditor.tsx index 83a0fa747a2..671bbdaf5b5 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationEditor.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationEditor.tsx @@ -13,7 +13,7 @@ import { } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { getTemplateSrv } from '@grafana/runtime'; -import { Icon, JSONFormatter, useStyles2 } from '@grafana/ui'; +import { Icon, JSONFormatter, useStyles2, Drawer } from '@grafana/ui'; import { TransformationsEditorTransformation } from './types'; @@ -24,6 +24,7 @@ interface TransformationEditorProps { uiConfig: TransformerRegistryItem; configs: TransformationsEditorTransformation[]; onChange: (index: number, config: DataTransformerConfig) => void; + toggleShowDebug: () => void; } export const TransformationEditor = ({ @@ -33,6 +34,7 @@ export const TransformationEditor = ({ uiConfig, configs, onChange, + toggleShowDebug, }: TransformationEditorProps) => { const styles = useStyles2(getStyles); const [input, setInput] = useState([]); @@ -84,32 +86,32 @@ export const TransformationEditor = ({
{editor} {debugMode && ( -
-
-
Transformation input data
-
- + +
+
+
Input data
+
+ +
+
+
+ +
+
+
Output data
+
{output && }
-
- -
-
-
Transformation output data
-
{output && }
-
-
+ )}
); }; const getStyles = (theme: GrafanaTheme2) => { - const debugBorder = theme.isLight ? theme.v1.palette.gray85 : theme.v1.palette.gray15; - return { title: css` display: flex; @@ -159,7 +161,7 @@ const getStyles = (theme: GrafanaTheme2) => { font-family: ${theme.typography.fontFamilyMonospace}; font-size: ${theme.typography.bodySmall.fontSize}; color: ${theme.colors.text}; - border-bottom: 1px solid ${debugBorder}; + border-bottom: 1px solid ${theme.colors.border.weak}; flex-grow: 0; flex-shrink: 1; `, @@ -167,7 +169,7 @@ const getStyles = (theme: GrafanaTheme2) => { debug: css` margin-top: ${theme.spacing(1)}; padding: 0 ${theme.spacing(1, 1, 1)}; - border: 1px solid ${debugBorder}; + border: 1px solid ${theme.colors.border.weak}; background: ${theme.isLight ? theme.v1.palette.white : theme.v1.palette.gray05}; border-radius: ${theme.shape.radius.default}; width: 100%; diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx index 9a05c7ce589..15d0410a9f9 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx @@ -9,10 +9,7 @@ import { QueryOperationAction, QueryOperationToggleAction, } from 'app/core/components/QueryOperationRow/QueryOperationAction'; -import { - QueryOperationRow, - QueryOperationRowRenderProps, -} from 'app/core/components/QueryOperationRow/QueryOperationRow'; +import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow'; import config from 'app/core/config'; import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo'; @@ -101,7 +98,7 @@ export const TransformationOperationRow = ({ [configs, index] ); - const renderActions = ({ isOpen }: QueryOperationRowRenderProps) => { + const renderActions = () => { return ( <> {uiConfig.state && } @@ -122,7 +119,6 @@ export const TransformationOperationRow = ({ )} ); From 71e3814c469e6d407cf5458951258c000ffc861c Mon Sep 17 00:00:00 2001 From: Kyle Cunningham Date: Fri, 27 Oct 2023 10:30:49 -0500 Subject: [PATCH 142/180] Transformations: Allow Timeseries to table transformation to handle multiple time series (#76801) * Initial fix up of getLabelFields * Update time series to table code * Cleanup and allow merging * Support merge and non-merge scenarios * Update editor * Fix case with merge true for multiple queries * Update time series detection, fix tests * Remove spurious console.log * Prettier plus remove test console.log * Remove type assertion * Add options migration * Add type export * Sentence casing * Make sure current options are preserved when making changes * Add disabled image * DashboardMigrator prettier * Add type assertion explanation and exception * Fix schema version test * Prettier * Fix genAI tests and make them more robust so they dont break on every new schema version --------- Co-authored-by: nmarrs --- .betterer.results | 9 +- packages/grafana-data/src/dataframe/utils.ts | 10 +- .../components/GenAI/jsonDiffText.test.ts | 3 +- .../dashboard/components/GenAI/utils.test.ts | 9 +- .../dashboard/state/DashboardMigrator.test.ts | 4 +- .../dashboard/state/DashboardMigrator.ts | 55 +++- .../TimeSeriesTableTransformEditor.tsx | 121 ++++++-- .../timeSeriesTableTransformer.test.ts | 52 ++-- .../timeSeriesTableTransformer.ts | 289 +++++++++++++----- .../disabled/timeSeriesTable.svg | 33 ++ 10 files changed, 439 insertions(+), 146 deletions(-) create mode 100644 public/img/transformations/disabled/timeSeriesTable.svg diff --git a/.betterer.results b/.betterer.results index 658ec507d17..28408dbcdcb 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3359,17 +3359,18 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "19"], [0, 0, 0, "Unexpected any. Specify a different type.", "20"], [0, 0, 0, "Unexpected any. Specify a different type.", "21"], - [0, 0, 0, "Unexpected any. Specify a different type.", "22"], + [0, 0, 0, "Do not use any type assertions.", "22"], [0, 0, 0, "Unexpected any. Specify a different type.", "23"], [0, 0, 0, "Unexpected any. Specify a different type.", "24"], [0, 0, 0, "Unexpected any. Specify a different type.", "25"], [0, 0, 0, "Unexpected any. Specify a different type.", "26"], [0, 0, 0, "Unexpected any. Specify a different type.", "27"], - [0, 0, 0, "Do not use any type assertions.", "28"], + [0, 0, 0, "Unexpected any. Specify a different type.", "28"], [0, 0, 0, "Do not use any type assertions.", "29"], - [0, 0, 0, "Unexpected any. Specify a different type.", "30"], + [0, 0, 0, "Do not use any type assertions.", "30"], [0, 0, 0, "Unexpected any. Specify a different type.", "31"], - [0, 0, 0, "Do not use any type assertions.", "32"] + [0, 0, 0, "Unexpected any. Specify a different type.", "32"], + [0, 0, 0, "Do not use any type assertions.", "33"] ], "public/app/features/dashboard/state/DashboardModel.repeat.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/packages/grafana-data/src/dataframe/utils.ts b/packages/grafana-data/src/dataframe/utils.ts index 774c6cd4b6d..6cd53545a4c 100644 --- a/packages/grafana-data/src/dataframe/utils.ts +++ b/packages/grafana-data/src/dataframe/utils.ts @@ -3,10 +3,16 @@ import { DataFrame, FieldType } from '../types/dataFrame'; import { getTimeField } from './processDataFrame'; export function isTimeSeriesFrame(frame: DataFrame) { - if (frame.fields.length > 2) { + // If we have less than two frames we can't have a timeseries + if (frame.fields.length < 2) { return false; } - return Boolean(frame.fields.find((field) => field.type === FieldType.time)); + + // In order to have a time series we need a time field + // and at least one number field + const timeField = frame.fields.find((field) => field.type === FieldType.time); + const numberField = frame.fields.find((field) => field.type === FieldType.number); + return timeField !== undefined && numberField !== undefined; } export function isTimeSeriesFrames(data: DataFrame[]) { diff --git a/public/app/features/dashboard/components/GenAI/jsonDiffText.test.ts b/public/app/features/dashboard/components/GenAI/jsonDiffText.test.ts index 96f09c6d92f..aa58a8a5fa3 100644 --- a/public/app/features/dashboard/components/GenAI/jsonDiffText.test.ts +++ b/public/app/features/dashboard/components/GenAI/jsonDiffText.test.ts @@ -1,3 +1,4 @@ +import { DASHBOARD_SCHEMA_VERSION } from '../../state/DashboardMigrator'; import { createDashboardModelFixture, createPanelSaveModel } from '../../state/__fixtures__/dashboardFixtures'; import { orderProperties, JSONArray, JSONValue, isObject, getDashboardStringDiff } from './jsonDiffText'; @@ -239,7 +240,7 @@ describe('isObject', () => { describe('getDashboardStringDiff', () => { const dashboard = { title: 'Original Title', - schemaVersion: 38, + schemaVersion: DASHBOARD_SCHEMA_VERSION, panels: [ createPanelSaveModel({ id: 1, diff --git a/public/app/features/dashboard/components/GenAI/utils.test.ts b/public/app/features/dashboard/components/GenAI/utils.test.ts index 0ad4eb886e4..41a2fb16f80 100644 --- a/public/app/features/dashboard/components/GenAI/utils.test.ts +++ b/public/app/features/dashboard/components/GenAI/utils.test.ts @@ -1,5 +1,6 @@ import { llms } from '@grafana/experimental'; +import { DASHBOARD_SCHEMA_VERSION } from '../../state/DashboardMigrator'; import { createDashboardModelFixture, createPanelSaveModel } from '../../state/__fixtures__/dashboardFixtures'; import { getDashboardChanges, isLLMPluginEnabled, sanitizeReply } from './utils'; @@ -21,7 +22,7 @@ describe('getDashboardChanges', () => { const deprecatedOptions = { legend: { displayMode: 'hidden', showLegend: false }, }; - const deprecatedVersion = 37; + const deprecatedVersion = DASHBOARD_SCHEMA_VERSION - 1; const dashboard = createDashboardModelFixture({ schemaVersion: deprecatedVersion, panels: [createPanelSaveModel({ title: 'Panel 1', options: deprecatedOptions })], @@ -48,8 +49,8 @@ describe('getDashboardChanges', () => { ' {\n' + ' "editable": true,\n' + ' "graphTooltip": 0,\n' + - '- "schemaVersion": 37,\n' + - '+ "schemaVersion": 38,\n' + + `- "schemaVersion": ${deprecatedVersion},\n` + + `+ "schemaVersion": ${DASHBOARD_SCHEMA_VERSION},\n` + ' "timezone": "",\n' + ' "panels": [\n' + ' {\n' + @@ -62,7 +63,7 @@ describe('getDashboardChanges', () => { '+++ After user changes\t\n' + '@@ -3,16 +3,17 @@\n' + ' "graphTooltip": 0,\n' + - ' "schemaVersion": 38,\n' + + ` "schemaVersion": ${DASHBOARD_SCHEMA_VERSION},\n` + ' "timezone": "",\n' + ' "panels": [\n' + ' {\n' + diff --git a/public/app/features/dashboard/state/DashboardMigrator.test.ts b/public/app/features/dashboard/state/DashboardMigrator.test.ts index cea0683ac3b..38440120815 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.test.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.test.ts @@ -13,6 +13,8 @@ import { VariableHide } from '../../variables/types'; import { DashboardModel } from '../state/DashboardModel'; import { PanelModel } from '../state/PanelModel'; +import { DASHBOARD_SCHEMA_VERSION } from './DashboardMigrator'; + jest.mock('app/core/services/context_srv', () => ({})); const dataSources = { @@ -228,7 +230,7 @@ describe('DashboardModel', () => { }); it('dashboard schema version should be set to latest', () => { - expect(model.schemaVersion).toBe(38); + expect(model.schemaVersion).toBe(DASHBOARD_SCHEMA_VERSION); }); it('graph thresholds should be migrated', () => { diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index 5ebed64deea..6eece6a4515 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -15,6 +15,7 @@ import { isEmptyObject, MappingType, PanelPlugin, + ReducerID, SpecialValueMatch, standardEditorsRegistry, standardFieldConfigEditorRegistry, @@ -42,6 +43,10 @@ import { import getFactors from 'app/core/utils/factors'; import kbn from 'app/core/utils/kbn'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { + RefIdTransformerOptions, + TimeSeriesTableTransformerOptions, +} from 'app/features/transformers/timeSeriesTable/timeSeriesTableTransformer'; import { isConstant, isMulti } from 'app/features/variables/guard'; import { alignCurrentWithMulti } from 'app/features/variables/shared/multiOptions'; import { CloudWatchMetricsQuery, LegacyAnnotationQuery } from 'app/plugins/datasource/cloudwatch/types'; @@ -63,6 +68,14 @@ standardEditorsRegistry.setInit(getAllOptionEditors); standardFieldConfigEditorRegistry.setInit(getAllStandardFieldConfigs); type PanelSchemeUpgradeHandler = (panel: PanelModel) => PanelModel; + +/** + * The current version of the dashboard schema. + * To add a dashboard migration increment this number + * and then add your migration at the bottom of 'updateSchema' + * hint: search "Add migration here" + */ +export const DASHBOARD_SCHEMA_VERSION = 39; export class DashboardMigrator { dashboard: DashboardModel; @@ -79,7 +92,7 @@ export class DashboardMigrator { let i, j, k, n; const oldVersion = this.dashboard.schemaVersion; const panelUpgrades: PanelSchemeUpgradeHandler[] = []; - this.dashboard.schemaVersion = 38; + this.dashboard.schemaVersion = DASHBOARD_SCHEMA_VERSION; if (oldVersion === this.dashboard.schemaVersion) { return; @@ -849,6 +862,46 @@ export class DashboardMigrator { }); } + // Update the configuration of the Timeseries to table transformation + // to support multiple options per query + if (oldVersion < 39) { + panelUpgrades.push((panel: PanelModel) => { + panel.transformations?.forEach((transformation) => { + // If we run into a timeSeriesTable transformation + // and it doesn't have undefined options then we migrate + if ( + transformation.id === 'timeSeriesTable' && + transformation.options !== undefined && + transformation.options.refIdToStat !== undefined + ) { + let tableTransformOptions: TimeSeriesTableTransformerOptions = {}; + + // For each {refIdtoStat} record which maps refId to a statistic + // we add that to the stat property of the the new + // RefIdTransformerOptions interface which includes multiple settings + for (const [refId, stat] of Object.entries(transformation.options.refIdToStat)) { + let newSettings: RefIdTransformerOptions = {}; + // In this case the easiest way is just to do a type + // assertion as iterated entries have unknown types + newSettings.stat = stat as ReducerID; + tableTransformOptions[refId] = newSettings; + } + + // Update the options + transformation.options = tableTransformOptions; + } + }); + + return panel; + }); + } + + /** + * -==- Add migration here -==- + * Your migration should go below the previous + * block and above this (hopefully) helpful message. + */ + if (panelUpgrades.length === 0) { return; } diff --git a/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx b/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx index 1d4d6ad5df2..13eefd29ef4 100644 --- a/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx +++ b/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx @@ -1,30 +1,63 @@ import React, { useCallback } from 'react'; -import { PluginState, TransformerRegistryItem, TransformerUIProps, ReducerID, isReducerID } from '@grafana/data'; -import { InlineFieldRow, InlineField, StatsPicker } from '@grafana/ui'; +import { + PluginState, + TransformerRegistryItem, + TransformerUIProps, + ReducerID, + isReducerID, + SelectableValue, + getFieldDisplayName, +} from '@grafana/data'; +import { InlineFieldRow, InlineField, StatsPicker, InlineSwitch, Select } from '@grafana/ui'; -import { timeSeriesTableTransformer, TimeSeriesTableTransformerOptions } from './timeSeriesTableTransformer'; +import { + timeSeriesTableTransformer, + TimeSeriesTableTransformerOptions, + getRefData, +} from './timeSeriesTableTransformer'; export function TimeSeriesTableTransformEditor({ input, options, onChange, }: TransformerUIProps) { - const refIds: string[] = input.reduce((acc, frame) => { - if (frame.refId && !acc.includes(frame.refId)) { - return [...acc, frame.refId]; + const timeFields: Array> = []; + const refIdMap = getRefData(input); + + // Retrieve time fields + for (const frame of input) { + for (const field of frame.fields) { + if (field.type === 'time') { + const name = getFieldDisplayName(field, frame, input); + timeFields.push({ label: name, value: name }); + } } - return acc; - }, []); + } + + const onSelectTimefield = useCallback( + (refId: string, value: SelectableValue) => { + const val = value?.value !== undefined ? value.value : ''; + onChange({ + ...options, + [refId]: { + ...options[refId], + timeField: val, + }, + }); + }, + [onChange, options] + ); const onSelectStat = useCallback( (refId: string, stats: string[]) => { const reducerID = stats[0]; if (reducerID && isReducerID(reducerID)) { onChange({ - refIdToStat: { - ...options.refIdToStat, - [refId]: reducerID, + ...options, + [refId]: { + ...options[refId], + stat: reducerID, }, }); } @@ -32,25 +65,55 @@ export function TimeSeriesTableTransformEditor({ [onChange, options] ); - return ( - <> - {refIds.map((refId) => { - return ( -
- - 1 ? ` #${refId}` : ''} value`}> - ext.id !== ReducerID.allValues && ext.id !== ReducerID.uniqueValues} - /> - - -
- ); - })} - + const onMergeSeriesToggle = useCallback( + (refId: string) => { + const mergeSeries = options[refId]?.mergeSeries !== undefined ? !options[refId].mergeSeries : false; + onChange({ + ...options, + [refId]: { + ...options[refId], + mergeSeries, + }, + }); + }, + [onChange, options] ); + + let configRows = []; + for (const refId of Object.keys(refIdMap)) { + configRows.push( + + +