diff --git a/.betterer.results b/.betterer.results index 3b2ceb4fc94..85a1ffd7e09 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2795,9 +2795,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "5"], [0, 0, 0, "Unexpected any. Specify a different type.", "6"] ], - "public/app/features/api-keys/ApiKeysForm.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/features/canvas/element.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/pkg/services/apikey/apikey.go b/pkg/services/apikey/apikey.go index a2ac556704b..ccdd3454ace 100644 --- a/pkg/services/apikey/apikey.go +++ b/pkg/services/apikey/apikey.go @@ -7,6 +7,7 @@ import ( type Service interface { GetAPIKeys(ctx context.Context, query *GetApiKeysQuery) error GetAllAPIKeys(ctx context.Context, orgID int64) ([]*APIKey, error) + CountAPIKeys(ctx context.Context, orgID int64) (int64, error) DeleteApiKey(ctx context.Context, cmd *DeleteCommand) error AddAPIKey(ctx context.Context, cmd *AddCommand) error GetApiKeyById(ctx context.Context, query *GetByIDQuery) error diff --git a/pkg/services/apikey/apikeyimpl/apikey.go b/pkg/services/apikey/apikeyimpl/apikey.go index 2a09d26319f..1472662628e 100644 --- a/pkg/services/apikey/apikeyimpl/apikey.go +++ b/pkg/services/apikey/apikeyimpl/apikey.go @@ -68,6 +68,9 @@ func (s *Service) AddAPIKey(ctx context.Context, cmd *apikey.AddCommand) error { func (s *Service) UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64) error { return s.store.UpdateAPIKeyLastUsedDate(ctx, tokenID) } +func (s *Service) CountAPIKeys(ctx context.Context, orgID int64) (int64, error) { + return s.store.CountAPIKeys(ctx, orgID) +} func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) { limits := "a.Map{} diff --git a/pkg/services/apikey/apikeyimpl/sqlx_store.go b/pkg/services/apikey/apikeyimpl/sqlx_store.go index 481b73360d1..7b43490ba48 100644 --- a/pkg/services/apikey/apikeyimpl/sqlx_store.go +++ b/pkg/services/apikey/apikeyimpl/sqlx_store.go @@ -63,6 +63,18 @@ func (ss *sqlxStore) GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey. return result, err } +func (ss *sqlxStore) CountAPIKeys(ctx context.Context, orgID int64) (int64, error) { + type result struct { + Count int64 + } + r := result{} + err := ss.sess.Get(ctx, &r, `SELECT COUNT(*) AS count FROM api_key WHERE service_account_id IS NULL and org_id = ?`, orgID) + if err != nil { + return 0, err + } + return r.Count, err +} + func (ss *sqlxStore) DeleteApiKey(ctx context.Context, cmd *apikey.DeleteCommand) error { res, err := ss.sess.Exec(ctx, "DELETE FROM api_key WHERE id=? and org_id=? and service_account_id IS NULL", cmd.ID, cmd.OrgID) if err != nil { diff --git a/pkg/services/apikey/apikeyimpl/store.go b/pkg/services/apikey/apikeyimpl/store.go index 54988660d08..fa3c33cae29 100644 --- a/pkg/services/apikey/apikeyimpl/store.go +++ b/pkg/services/apikey/apikeyimpl/store.go @@ -10,6 +10,7 @@ import ( type store interface { GetAPIKeys(ctx context.Context, query *apikey.GetApiKeysQuery) error GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey.APIKey, error) + CountAPIKeys(ctx context.Context, orgID int64) (int64, error) DeleteApiKey(ctx context.Context, cmd *apikey.DeleteCommand) error AddAPIKey(ctx context.Context, cmd *apikey.AddCommand) error GetApiKeyById(ctx context.Context, query *apikey.GetByIDQuery) error diff --git a/pkg/services/apikey/apikeyimpl/xorm_store.go b/pkg/services/apikey/apikeyimpl/xorm_store.go index 73298cc15c8..61b521a0b56 100644 --- a/pkg/services/apikey/apikeyimpl/xorm_store.go +++ b/pkg/services/apikey/apikeyimpl/xorm_store.go @@ -65,6 +65,25 @@ func (ss *sqlStore) GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey.A return result, err } +func (ss *sqlStore) CountAPIKeys(ctx context.Context, orgID int64) (int64, error) { + type result struct { + Count int64 + } + + r := result{} + err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + rawSQL := "SELECT COUNT(*) AS count FROM api_key WHERE org_id = ? and service_account_id IS NULL" + if _, err := sess.SQL(rawSQL, orgID).Get(&r); err != nil { + return err + } + return nil + }) + if err != nil { + return 0, err + } + return r.Count, err +} + func (ss *sqlStore) DeleteApiKey(ctx context.Context, cmd *apikey.DeleteCommand) error { return ss.db.WithDbSession(ctx, func(sess *db.Session) error { rawSQL := "DELETE FROM api_key WHERE id=? and org_id=? and service_account_id IS NULL" diff --git a/pkg/services/apikey/apikeytest/fake.go b/pkg/services/apikey/apikeytest/fake.go index 35ed0ec76e0..00f14b326f6 100644 --- a/pkg/services/apikey/apikeytest/fake.go +++ b/pkg/services/apikey/apikeytest/fake.go @@ -8,6 +8,7 @@ import ( type Service struct { ExpectedError error + ExpectedCount int64 ExpectedAPIKeys []*apikey.APIKey ExpectedAPIKey *apikey.APIKey } @@ -19,6 +20,9 @@ func (s *Service) GetAPIKeys(ctx context.Context, query *apikey.GetApiKeysQuery) func (s *Service) GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey.APIKey, error) { return s.ExpectedAPIKeys, s.ExpectedError } +func (s *Service) CountAPIKeys(ctx context.Context, orgID int64) (int64, error) { + return s.ExpectedCount, s.ExpectedError +} func (s *Service) GetApiKeyById(ctx context.Context, query *apikey.GetByIDQuery) error { query.Result = s.ExpectedAPIKey return s.ExpectedError diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index 4c995cdbf5b..6f6e9164a15 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -80,12 +80,13 @@ func (s *ServiceImpl) getOrgAdminNode(c *contextmodel.ReqContext) (*navtree.NavL } hideApiKeys, _, _ := s.kvStore.Get(c.Req.Context(), c.OrgID, "serviceaccounts", "hideApiKeys") - apiKeys, err := s.apiKeyService.GetAllAPIKeys(c.Req.Context(), c.OrgID) + apiKeys, err := s.apiKeyService.CountAPIKeys(c.Req.Context(), c.OrgID) if err != nil { return nil, err } - apiKeysHidden := hideApiKeys == "1" && len(apiKeys) == 0 + // Hide API keys if the global setting is set or if the org setting is set and there are no API keys + apiKeysHidden := hideApiKeys == "1" && apiKeys == 0 if hasAccess(ac.ReqOrgAdmin, ac.ApiKeyAccessEvaluator) && !apiKeysHidden { configNodes = append(configNodes, &navtree.NavLink{ Text: "API keys", diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index b11665388ca..706a61eb8be 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -41,7 +41,6 @@ type service interface { 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 - GetAPIKeysMigrationStatus(ctx context.Context, orgID int64) (*serviceaccounts.APIKeysMigrationStatus, error) HideApiKeysTab(ctx context.Context, orgID int64) error MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) error MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error @@ -89,8 +88,6 @@ func (api *ServiceAccountsAPI) RegisterAPIEndpoints() { accesscontrol.EvalPermission(serviceaccounts.ActionWrite, serviceaccounts.ScopeID)), routing.Wrap(api.CreateToken)) serviceAccountsRoute.Delete("/:serviceAccountId/tokens/:tokenId", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionWrite, serviceaccounts.ScopeID)), routing.Wrap(api.DeleteToken)) - serviceAccountsRoute.Get("/migrationstatus", auth(middleware.ReqOrgAdmin, - accesscontrol.EvalPermission(serviceaccounts.ActionRead)), routing.Wrap(api.GetAPIKeysMigrationStatus)) serviceAccountsRoute.Post("/hideApiKeys", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionCreate)), routing.Wrap(api.HideApiKeysTab)) serviceAccountsRoute.Post("/migrate", auth(middleware.ReqOrgAdmin, @@ -364,15 +361,6 @@ func (api *ServiceAccountsAPI) SearchOrgServiceAccountsWithPaging(c *contextmode return response.JSON(http.StatusOK, serviceAccountSearch) } -// GET /api/serviceaccounts/migrationstatus -func (api *ServiceAccountsAPI) GetAPIKeysMigrationStatus(ctx *contextmodel.ReqContext) response.Response { - upgradeStatus, err := api.service.GetAPIKeysMigrationStatus(ctx.Req.Context(), ctx.OrgID) - if err != nil { - return response.Error(http.StatusInternalServerError, "Internal server error", err) - } - return response.JSON(http.StatusOK, upgradeStatus) -} - // POST /api/serviceaccounts/hideapikeys func (api *ServiceAccountsAPI) HideApiKeysTab(ctx *contextmodel.ReqContext) response.Response { if err := api.service.HideApiKeysTab(ctx.Req.Context(), ctx.OrgID); err != nil { diff --git a/pkg/services/serviceaccounts/api/api_test.go b/pkg/services/serviceaccounts/api/api_test.go index f6f97f40928..7dc75acc2be 100644 --- a/pkg/services/serviceaccounts/api/api_test.go +++ b/pkg/services/serviceaccounts/api/api_test.go @@ -81,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 = &fakeService{ExpectedServiceAccount: tt.expectedSA, ExpectedErr: tt.expectedErr} + a.service = &fakeServiceAccountService{ExpectedServiceAccount: tt.expectedSA, ExpectedErr: tt.expectedErr} }) req := server.NewRequest(http.MethodPost, "/api/serviceaccounts/", strings.NewReader(tt.body)) webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgRole: tt.basicRole, OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}) @@ -159,7 +159,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 = &fakeService{ExpectedServiceAccountProfile: tt.expectedSA} + a.service = &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)}}) @@ -221,7 +221,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 = &fakeService{ExpectedServiceAccountProfile: tt.expectedSA} + a.service = &fakeServiceAccountService{ExpectedServiceAccountProfile: tt.expectedSA} }) req := server.NewRequest(http.MethodPatch, fmt.Sprintf("/api/serviceaccounts/%d", tt.id), strings.NewReader(tt.body)) @@ -240,7 +240,7 @@ func setupTests(t *testing.T, opts ...func(a *ServiceAccountsAPI)) *webtest.Serv cfg := setting.NewCfg() api := &ServiceAccountsAPI{ cfg: cfg, - service: &fakeService{}, + service: &fakeServiceAccountService{}, accesscontrolService: &actest.FakeService{}, accesscontrol: acimpl.ProvideAccessControl(cfg), RouterRegister: routing.NewRouteRegister(), @@ -255,9 +255,9 @@ func setupTests(t *testing.T, opts ...func(a *ServiceAccountsAPI)) *webtest.Serv return webtest.NewServer(t, api.RouterRegister) } -var _ service = new(fakeService) +var _ service = new(fakeServiceAccountService) -type fakeService struct { +type fakeServiceAccountService struct { service ExpectedErr error ExpectedAPIKey *apikey.APIKey @@ -266,30 +266,30 @@ type fakeService struct { ExpectedServiceAccountProfile *serviceaccounts.ServiceAccountProfileDTO } -func (f *fakeService) CreateServiceAccount(ctx context.Context, orgID int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { +func (f *fakeServiceAccountService) CreateServiceAccount(ctx context.Context, orgID int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { return f.ExpectedServiceAccount, f.ExpectedErr } -func (f *fakeService) DeleteServiceAccount(ctx context.Context, orgID, id int64) error { +func (f *fakeServiceAccountService) DeleteServiceAccount(ctx context.Context, orgID, id int64) error { return f.ExpectedErr } -func (f *fakeService) RetrieveServiceAccount(ctx context.Context, orgID, id int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { +func (f *fakeServiceAccountService) RetrieveServiceAccount(ctx context.Context, orgID, id int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { return f.ExpectedServiceAccountProfile, f.ExpectedErr } -func (f *fakeService) ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error) { +func (f *fakeServiceAccountService) ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error) { return f.ExpectedServiceAccountTokens, f.ExpectedErr } -func (f *fakeService) UpdateServiceAccount(ctx context.Context, orgID, id int64, cmd *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) { +func (f *fakeServiceAccountService) UpdateServiceAccount(ctx context.Context, orgID, id int64, cmd *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) { return f.ExpectedServiceAccountProfile, f.ExpectedErr } -func (f *fakeService) AddServiceAccountToken(ctx context.Context, id int64, cmd *serviceaccounts.AddServiceAccountTokenCommand) (*apikey.APIKey, error) { +func (f *fakeServiceAccountService) AddServiceAccountToken(ctx context.Context, id int64, cmd *serviceaccounts.AddServiceAccountTokenCommand) (*apikey.APIKey, error) { return f.ExpectedAPIKey, f.ExpectedErr } -func (f *fakeService) DeleteServiceAccountToken(ctx context.Context, orgID, id, tokenID int64) error { +func (f *fakeServiceAccountService) DeleteServiceAccountToken(ctx context.Context, orgID, id, tokenID int64) error { return f.ExpectedErr } diff --git a/pkg/services/serviceaccounts/api/token_test.go b/pkg/services/serviceaccounts/api/token_test.go index 79ce0ef1bb7..bcce9ebf9bb 100644 --- a/pkg/services/serviceaccounts/api/token_test.go +++ b/pkg/services/serviceaccounts/api/token_test.go @@ -43,7 +43,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 = &fakeService{} + a.service = &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 +109,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 = &fakeService{ + a.service = &fakeServiceAccountService{ ExpectedErr: tt.expectedErr, ExpectedAPIKey: tt.expectedAPIKey, } @@ -163,7 +163,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 = &fakeService{ExpectedErr: tt.expectedErr} + a.service = &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/database/store.go b/pkg/services/serviceaccounts/database/store.go index c9e5b05cf13..c2a16aa436a 100644 --- a/pkg/services/serviceaccounts/database/store.go +++ b/pkg/services/serviceaccounts/database/store.go @@ -388,22 +388,6 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context, return searchResult, nil } -func (s *ServiceAccountsStoreImpl) GetAPIKeysMigrationStatus(ctx context.Context, orgId int64) (status *serviceaccounts.APIKeysMigrationStatus, err error) { - migrationStatus, exists, err := s.kvStore.Get(ctx, orgId, "serviceaccounts", "migrationStatus") - if err != nil { - return nil, err - } - if exists && migrationStatus == "1" { - return &serviceaccounts.APIKeysMigrationStatus{ - Migrated: true, - }, nil - } else { - return &serviceaccounts.APIKeysMigrationStatus{ - Migrated: false, - }, nil - } -} - func (s *ServiceAccountsStoreImpl) HideApiKeysTab(ctx context.Context, orgId int64) error { if err := s.kvStore.Set(ctx, orgId, "serviceaccounts", "hideApiKeys", "1"); err != nil { s.log.Error("Failed to hide API keys tab", err) diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index 74560bfb7f8..031b4c7bcde 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -226,13 +226,6 @@ func (sa *ServiceAccountsService) DeleteServiceAccountToken(ctx context.Context, return sa.store.DeleteServiceAccountToken(ctx, orgID, serviceAccountID, tokenID) } -func (sa *ServiceAccountsService) GetAPIKeysMigrationStatus(ctx context.Context, orgID int64) (status *serviceaccounts.APIKeysMigrationStatus, err error) { - if err := validOrgID(orgID); err != nil { - return nil, err - } - return sa.store.GetAPIKeysMigrationStatus(ctx, orgID) -} - func (sa *ServiceAccountsService) HideApiKeysTab(ctx context.Context, orgID int64) error { if err := validOrgID(orgID); err != nil { return err diff --git a/pkg/services/serviceaccounts/manager/service_test.go b/pkg/services/serviceaccounts/manager/service_test.go index 1e5d68a4b55..9d7f806385c 100644 --- a/pkg/services/serviceaccounts/manager/service_test.go +++ b/pkg/services/serviceaccounts/manager/service_test.go @@ -17,10 +17,10 @@ type FakeServiceAccountStore struct { ExpectedServiceAccountDTO *serviceaccounts.ServiceAccountDTO ExpectedServiceAccountProfileDTO *serviceaccounts.ServiceAccountProfileDTO ExpectedSearchServiceAccountQueryResult *serviceaccounts.SearchOrgServiceAccountsResult - ExpectedServiceAccountMigrationStatus *serviceaccounts.APIKeysMigrationStatus ExpectedStats *serviceaccounts.Stats ExpectedAPIKeys []apikey.APIKey ExpectedAPIKey *apikey.APIKey + ExpectedBoolean bool ExpectedError error } @@ -59,11 +59,6 @@ func (f *FakeServiceAccountStore) DeleteServiceAccount(ctx context.Context, orgI return f.ExpectedError } -// GetAPIKeysMigrationStatus is a fake getting the api keys migration status. -func (f *FakeServiceAccountStore) GetAPIKeysMigrationStatus(ctx context.Context, orgID int64) (*serviceaccounts.APIKeysMigrationStatus, error) { - return f.ExpectedServiceAccountMigrationStatus, f.ExpectedError -} - // HideApiKeysTab is a fake hiding the api keys tab. func (f *FakeServiceAccountStore) HideApiKeysTab(ctx context.Context, orgID int64) error { return f.ExpectedError diff --git a/pkg/services/serviceaccounts/manager/store.go b/pkg/services/serviceaccounts/manager/store.go index 7ddb53ceb34..0168d334366 100644 --- a/pkg/services/serviceaccounts/manager/store.go +++ b/pkg/services/serviceaccounts/manager/store.go @@ -26,7 +26,6 @@ type store interface { 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 - GetAPIKeysMigrationStatus(ctx context.Context, orgID int64) (*serviceaccounts.APIKeysMigrationStatus, error) HideApiKeysTab(ctx context.Context, orgID int64) error MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) error MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error diff --git a/pkg/services/serviceaccounts/models.go b/pkg/services/serviceaccounts/models.go index 68a7ab64411..1239cb72650 100644 --- a/pkg/services/serviceaccounts/models.go +++ b/pkg/services/serviceaccounts/models.go @@ -130,10 +130,6 @@ type ServiceAccountProfileDTO struct { type ServiceAccountFilter string // used for filtering -type APIKeysMigrationStatus struct { - Migrated bool `json:"migrated"` -} - const ( FilterOnlyExpiredTokens ServiceAccountFilter = "expiredTokens" FilterOnlyDisabled ServiceAccountFilter = "disabled" diff --git a/public/app/core/components/Footer/Footer.tsx b/public/app/core/components/Footer/Footer.tsx index 0b9e873d351..3ebe9e3b733 100644 --- a/public/app/core/components/Footer/Footer.tsx +++ b/public/app/core/components/Footer/Footer.tsx @@ -55,7 +55,7 @@ export function getVersionLinks(): FooterLink[] { links.push({ target: '_blank', - id: 'version', + id: 'license', text: `${buildInfo.edition}${stateInfo}`, url: licenseInfo.licenseUrl, }); diff --git a/public/app/features/api-keys/APIKeysMigratedCard.tsx b/public/app/features/api-keys/APIKeysMigratedCard.tsx index c2bfba21093..a6cd305c6e7 100644 --- a/public/app/features/api-keys/APIKeysMigratedCard.tsx +++ b/public/app/features/api-keys/APIKeysMigratedCard.tsx @@ -6,29 +6,31 @@ import { Alert, ConfirmModal, useStyles2, Button } from '@grafana/ui'; interface Props { onHideApiKeys: () => void; + apikeys: number; } -export const APIKeysMigratedCard = ({ onHideApiKeys }: Props): JSX.Element => { +export const APIKeysMigratedCard = ({ onHideApiKeys, apikeys }: Props): JSX.Element => { const [isModalOpen, setIsModalOpen] = useState(false); const styles = useStyles2(getStyles); return ( - +
- We have migrated API keys into Grafana service accounts. All API keys are safe and continue working as they used - to, you can find them inside the respective service account. + Migrated API keys are safe and continue working as they used to. You can find them inside the respective service + account.
- setIsModalOpen(false)} + confirmButtonVariant="primary" /> View service accounts page
diff --git a/public/app/features/api-keys/ApiKeysActionBar.tsx b/public/app/features/api-keys/ApiKeysActionBar.tsx index ef182d1c799..17642fe2b24 100644 --- a/public/app/features/api-keys/ApiKeysActionBar.tsx +++ b/public/app/features/api-keys/ApiKeysActionBar.tsx @@ -1,23 +1,19 @@ import React, { FC } from 'react'; -import { Button, FilterInput } from '@grafana/ui'; +import { FilterInput } from '@grafana/ui'; interface Props { searchQuery: string; disabled: boolean; - onAddClick: () => void; onSearchChange: (value: string) => void; } -export const ApiKeysActionBar: FC = ({ searchQuery, disabled, onAddClick, onSearchChange }) => { +export const ApiKeysActionBar: FC = ({ searchQuery, disabled, onSearchChange }) => { return (
-
); }; diff --git a/public/app/features/api-keys/ApiKeysAddedModal.test.tsx b/public/app/features/api-keys/ApiKeysAddedModal.test.tsx deleted file mode 100644 index b6342923402..00000000000 --- a/public/app/features/api-keys/ApiKeysAddedModal.test.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import React from 'react'; - -import { ApiKeysAddedModal, Props } from './ApiKeysAddedModal'; - -describe('ApiKeysAddedModal', () => { - const props: Props = { - onDismiss: jest.fn(), - apiKey: 'myApiKey', - rootPath: 'test/path', - }; - - it('should render without throwing', () => { - expect(() => render()).not.toThrow(); - }); - - it('displays the apiKey in a readOnly input', () => { - render(); - const input = screen.getByRole('textbox'); - expect(input).toHaveValue(props.apiKey); - expect(input).toHaveAttribute('readonly'); - }); - - it('has a `Copy to clipboard` button', () => { - render(); - expect(screen.getByRole('button', { name: 'Copy' })).toBeInTheDocument(); - }); - - it('displays the correct curl path', () => { - render(); - expect( - screen.getByText('curl -H "Authorization: Bearer myApiKey" test/path/api/dashboards/home') - ).toBeInTheDocument(); - }); - - it('calls onDismiss when the modal is closed', () => { - render(); - screen.getByRole('button', { name: 'Close dialogue' }).click(); - expect(props.onDismiss).toHaveBeenCalled(); - }); -}); diff --git a/public/app/features/api-keys/ApiKeysAddedModal.tsx b/public/app/features/api-keys/ApiKeysAddedModal.tsx deleted file mode 100644 index ce04d4bd7ef..00000000000 --- a/public/app/features/api-keys/ApiKeysAddedModal.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { css } from '@emotion/css'; -import React, { useCallback } from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Alert, Field, Modal, useStyles2, Input, ClipboardButton } from '@grafana/ui'; - -export interface Props { - onDismiss: () => void; - apiKey: string; - rootPath: string; -} - -export function ApiKeysAddedModal({ onDismiss, apiKey, rootPath }: Props): JSX.Element { - const styles = useStyles2(getStyles); - const getClipboardText = useCallback(() => apiKey, [apiKey]); - - return ( - - - - Copy - - } - /> - - - It is not stored in this form, so be sure to copy it now. - - -

You can authenticate a request using the Authorization HTTP header, example:

-
-        curl -H "Authorization: Bearer {apiKey}" {rootPath}/api/dashboards/home
-      
-
- ); -} - -function getStyles(theme: GrafanaTheme2) { - return { - label: css` - padding: ${theme.spacing(1)}; - background-color: ${theme.colors.background.secondary}; - border-radius: ${theme.shape.borderRadius()}; - `, - small: css` - font-size: ${theme.typography.bodySmall.fontSize}; - font-weight: ${theme.typography.bodySmall.fontWeight}; - `, - }; -} diff --git a/public/app/features/api-keys/ApiKeysController.tsx b/public/app/features/api-keys/ApiKeysController.tsx index 69ac05514cb..6b1c65c3b6e 100644 --- a/public/app/features/api-keys/ApiKeysController.tsx +++ b/public/app/features/api-keys/ApiKeysController.tsx @@ -10,6 +10,8 @@ interface Props { } export const ApiKeysController: FC = ({ children }) => { + // FIXME(eleijonmarck): could not remove state from this component + // as component cannot render properly without it const [isAdding, setIsAdding] = useState(false); const toggleIsAdding = useCallback(() => { setIsAdding(!isAdding); diff --git a/public/app/features/api-keys/ApiKeysForm.tsx b/public/app/features/api-keys/ApiKeysForm.tsx deleted file mode 100644 index 973c4a9004f..00000000000 --- a/public/app/features/api-keys/ApiKeysForm.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import React, { ChangeEvent, FC, FormEvent, useEffect, useState } from 'react'; - -import { rangeUtil, SelectableValue } from '@grafana/data'; -import { EventsWithValidation, LegacyForms, ValidationEvents, Button, Select, InlineField } from '@grafana/ui'; -import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; - -import { SlideDown } from '../../core/components/Animations/SlideDown'; -import { NewApiKey, OrgRole } from '../../types'; - -const { Input } = LegacyForms; -const ROLE_OPTIONS: Array> = Object.keys(OrgRole).map((role) => ({ - label: role, - value: role as OrgRole, -})); - -interface Props { - show: boolean; - onClose: () => void; - onKeyAdded: (apiKey: NewApiKey) => void; - disabled: boolean; -} - -function isValidInterval(value: string): boolean { - if (!value) { - return true; - } - try { - rangeUtil.intervalToSeconds(value); - return true; - } catch {} - return false; -} - -const timeRangeValidationEvents: ValidationEvents = { - [EventsWithValidation.onBlur]: [ - { - rule: isValidInterval, - errorMessage: 'Not a valid duration', - }, - ], -}; - -const tooltipText = - 'The API key life duration. For example, 1d if your key is going to last for one day. Supported units are: s,m,h,d,w,M,y'; - -export const ApiKeysForm: FC = ({ show, onClose, onKeyAdded, disabled }) => { - const [name, setName] = useState(''); - const [role, setRole] = useState(OrgRole.Viewer); - const [secondsToLive, setSecondsToLive] = useState(''); - useEffect(() => { - setName(''); - setRole(OrgRole.Viewer); - setSecondsToLive(''); - }, [show]); - - const onSubmit = (event: FormEvent) => { - event.preventDefault(); - if (isValidInterval(secondsToLive)) { - onKeyAdded({ name, role, secondsToLive }); - onClose(); - } - }; - const onNameChange = (event: ChangeEvent) => { - setName(event.currentTarget.value); - }; - const onRoleChange = (role: SelectableValue) => { - setRole(role.value!); - }; - const onSecondsToLiveChange = (event: ChangeEvent) => { - setSecondsToLive(event.currentTarget.value); - }; - - return ( - -
- -
-
Add API Key
-
-
- Key name - -
-
- - - -
-
- -
-
-
-
-
- ); -}; diff --git a/public/app/features/api-keys/ApiKeysPage.test.tsx b/public/app/features/api-keys/ApiKeysPage.test.tsx index df8cf679c4c..f4369e75487 100644 --- a/public/app/features/api-keys/ApiKeysPage.test.tsx +++ b/public/app/features/api-keys/ApiKeysPage.test.tsx @@ -3,7 +3,6 @@ import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event' import React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; -import { selectors } from '@grafana/e2e-selectors'; import { ApiKey, OrgRole } from 'app/types'; import { mockToolkitActionCreator } from '../../../test/core/redux/mocks'; @@ -30,7 +29,6 @@ const setup = (propOverrides: Partial) => { const migrateAllMock = jest.fn(); const toggleIncludeExpiredMock = jest.fn(); const setSearchQueryMock = mockToolkitActionCreator(setSearchQuery); - const getApiKeysMigrationStatusMock = jest.fn(); const hideApiKeysMock = jest.fn(); const props: Props = { apiKeys: [] as ApiKey[], @@ -39,8 +37,6 @@ const setup = (propOverrides: Partial) => { loadApiKeys: loadApiKeysMock, deleteApiKey: deleteApiKeyMock, setSearchQuery: setSearchQueryMock, - addApiKey: addApiKeyMock, - getApiKeysMigrationStatus: getApiKeysMigrationStatusMock, migrateApiKey: migrateApiKeyMock, migrateAll: migrateAllMock, hideApiKeys: hideApiKeysMock, @@ -50,7 +46,6 @@ const setup = (propOverrides: Partial) => { includeExpiredDisabled: false, toggleIncludeExpired: toggleIncludeExpiredMock, canCreate: true, - apiKeysMigrated: false, }; Object.assign(props, propOverrides); @@ -87,13 +82,6 @@ describe('ApiKeysPage', () => { }); }); - describe('when there are no API keys', () => { - it('then it should render CTA', () => { - setup({ apiKeys: getMultipleMockKeys(0), apiKeysCount: 0, hasFetched: true }); - expect(screen.getByTestId(selectors.components.CallToActionCard.buttonV2('New API key'))).toBeInTheDocument(); - }); - }); - describe('when there are API keys', () => { it('then it should render API keys table', async () => { const apiKeys = [ @@ -165,66 +153,9 @@ describe('ApiKeysPage', () => { expect(deleteApiKeyMock).toHaveBeenCalledWith(2); }); }); - - describe('when a user adds an API key from CTA', () => { - it('then it should call addApiKey with correct parameters', async () => { - const apiKeys: ApiKey[] = []; - const { addApiKeyMock } = setup({ apiKeys, apiKeysCount: apiKeys.length, hasFetched: true }); - - addApiKeyMock.mockClear(); - await userEvent.click(screen.getByTestId(selectors.components.CallToActionCard.buttonV2('New API key'))); - await addAndVerifyApiKey(addApiKeyMock); - }); - }); - - describe('when a user adds an API key from Add API key', () => { - it('then it should call addApiKey with correct parameters', async () => { - const apiKeys = getMultipleMockKeys(1); - const { addApiKeyMock } = setup({ apiKeys, apiKeysCount: apiKeys.length, hasFetched: true }); - - addApiKeyMock.mockClear(); - await userEvent.click(screen.getByRole('button', { name: /add api key/i })); - await addAndVerifyApiKey(addApiKeyMock); - - await toggleShowExpired(); - - addApiKeyMock.mockClear(); - await userEvent.click(screen.getByRole('button', { name: /add api key/i })); - await addAndVerifyApiKey(addApiKeyMock); - }); - }); - - describe('when a user adds an API key with an invalid expiration', () => { - it('then it should display a message', async () => { - const apiKeys = getMultipleMockKeys(1); - const { addApiKeyMock } = setup({ apiKeys, apiKeysCount: apiKeys.length, hasFetched: true }); - - addApiKeyMock.mockClear(); - await userEvent.click(screen.getByRole('button', { name: /add api key/i })); - await userEvent.type(screen.getByPlaceholderText(/name/i), 'Test'); - await userEvent.type(screen.getByPlaceholderText(/1d/i), '60x'); - expect(screen.queryByText(/not a valid duration/i)).not.toBeInTheDocument(); - await userEvent.click(screen.getByRole('button', { name: /^add$/i })); - expect(screen.getByText(/not a valid duration/i)).toBeInTheDocument(); - expect(addApiKeyMock).toHaveBeenCalledTimes(0); - }); - }); }); async function toggleShowExpired() { expect(screen.queryByLabelText(/include expired keys/i)).toBeInTheDocument(); await userEvent.click(screen.getByLabelText(/include expired keys/i)); } - -async function addAndVerifyApiKey(addApiKeyMock: jest.Mock) { - expect(screen.getByRole('heading', { name: /add api key/i })).toBeInTheDocument(); - expect(screen.getByPlaceholderText(/name/i)).toBeInTheDocument(); - expect(screen.getByPlaceholderText(/1d/i)).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /^add$/i })).toBeInTheDocument(); - - await userEvent.type(screen.getByPlaceholderText(/name/i), 'Test'); - await userEvent.type(screen.getByPlaceholderText(/1d/i), '60s'); - await userEvent.click(screen.getByRole('button', { name: /^add$/i })); - expect(addApiKeyMock).toHaveBeenCalledTimes(1); - expect(addApiKeyMock).toHaveBeenCalledWith({ name: 'Test', role: 'Viewer', secondsToLive: 60 }, expect.anything()); -} diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index c3030d4e2bd..012441f2990 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -2,33 +2,24 @@ import React, { PureComponent } from 'react'; import { connect, ConnectedProps } from 'react-redux'; // Utils -import { rangeUtil } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { InlineField, InlineSwitch, VerticalGroup } from '@grafana/ui'; -import appEvents from 'app/core/app_events'; -import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import { Page } from 'app/core/components/Page/Page'; -import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; import { getTimeZone } from 'app/features/profile/state/selectors'; -import { AccessControlAction, ApiKey, NewApiKey, StoreState } from 'app/types'; -import { ShowModalReactEvent } from 'app/types/events'; +import { AccessControlAction, ApiKey, StoreState } from 'app/types'; import { APIKeysMigratedCard } from './APIKeysMigratedCard'; import { ApiKeysActionBar } from './ApiKeysActionBar'; -import { ApiKeysAddedModal } from './ApiKeysAddedModal'; import { ApiKeysController } from './ApiKeysController'; -import { ApiKeysForm } from './ApiKeysForm'; import { ApiKeysTable } from './ApiKeysTable'; import { MigrateToServiceAccountsCard } from './MigrateToServiceAccountsCard'; import { - addApiKey, deleteApiKey, migrateApiKey, migrateAll, loadApiKeys, toggleIncludeExpired, - getApiKeysMigrationStatus, hideApiKeys, } from './state/actions'; import { setSearchQuery } from './state/reducers'; @@ -46,7 +37,6 @@ function mapStateToProps(state: StoreState) { includeExpired: getIncludeExpired(state.apiKeys), includeExpiredDisabled: getIncludeExpiredDisabled(state.apiKeys), canCreate: canCreate, - apiKeysMigrated: state.apiKeys.apiKeysMigrated, }; } @@ -61,8 +51,6 @@ const mapDispatchToProps = { migrateAll, setSearchQuery, toggleIncludeExpired, - addApiKey, - getApiKeysMigrationStatus, hideApiKeys, }; @@ -83,7 +71,6 @@ export class ApiKeysPageUnconnected extends PureComponent { componentDidMount() { this.fetchApiKeys(); - this.props.getApiKeysMigrationStatus(); } async fetchApiKeys() { @@ -110,40 +97,6 @@ export class ApiKeysPageUnconnected extends PureComponent { this.props.toggleIncludeExpired(); }; - onAddApiKey = (newApiKey: NewApiKey) => { - const openModal = (apiKey: string) => { - const rootPath = window.location.origin + config.appSubUrl; - - appEvents.publish( - new ShowModalReactEvent({ - props: { - apiKey, - rootPath, - }, - component: ApiKeysAddedModal, - }) - ); - }; - - const secondsToLive = newApiKey.secondsToLive; - try { - const secondsToLiveAsNumber = secondsToLive ? rangeUtil.intervalToSeconds(secondsToLive) : null; - const apiKey: ApiKey = { - ...newApiKey, - secondsToLive: secondsToLiveAsNumber, - }; - this.props.addApiKey(apiKey, openModal); - this.setState((prevState: State) => { - return { - ...prevState, - isAdding: false, - }; - }); - } catch (err) { - console.error(err); - } - }; - onHideApiKeys = async () => { try { await this.props.hideApiKeys(); @@ -165,7 +118,6 @@ export class ApiKeysPageUnconnected extends PureComponent { includeExpired, includeExpiredDisabled, canCreate, - apiKeysMigrated, } = this.props; if (!hasFetched) { @@ -180,37 +132,21 @@ export class ApiKeysPageUnconnected extends PureComponent { - {({ isAdding, toggleIsAdding }) => { - const showCTA = !isAdding && apiKeysCount === 0 && !apiKeysMigrated; + {({}) => { const showTable = apiKeysCount > 0; return ( <> - {!apiKeysMigrated && } - {apiKeysMigrated && } - {showCTA ? ( - - ) : null} + {apiKeysCount !== 0 && } + {apiKeysCount === 0 && ( + + )} {showTable ? ( ) : null} - {showTable ? ( diff --git a/public/app/features/api-keys/MigrateToServiceAccountsCard.tsx b/public/app/features/api-keys/MigrateToServiceAccountsCard.tsx index 6721907a709..b76e470e913 100644 --- a/public/app/features/api-keys/MigrateToServiceAccountsCard.tsx +++ b/public/app/features/api-keys/MigrateToServiceAccountsCard.tsx @@ -16,26 +16,24 @@ export const MigrateToServiceAccountsCard = ({ onMigrate, disabled }: Props): JS const docsLink = ( - here. + Find out more about the migration here. ); - const migrationBoxDesc = ( - Are you sure you want to migrate all API keys to service accounts? Find out more {docsLink} - ); + const migrationBoxDesc = Are you sure you want to migrate all API keys to service accounts? {docsLink}; return ( - +
- Each API key will be automatically migrated into a service account with a token. The service account will be - created with the same permission as the API Key and current API Keys will continue to work as they were. + We will soon deprecate API keys. Each API key will be migrated into a service account with a token and will + continue to work as they were. We encourage you to migrate your API keys to service accounts now. {docsLink}
setIsModalOpen(false)} + confirmVariant="primary" + confirmButtonVariant="primary" />
diff --git a/public/app/features/api-keys/state/actions.ts b/public/app/features/api-keys/state/actions.ts index 381fc17e385..65f90b415cf 100644 --- a/public/app/features/api-keys/state/actions.ts +++ b/public/app/features/api-keys/state/actions.ts @@ -1,24 +1,7 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; -import store from 'app/core/store'; -import { API_KEYS_MIGRATION_INFO_STORAGE_KEY } from 'app/features/serviceaccounts/constants'; -import { ApiKey, ThunkResult } from 'app/types'; +import { ThunkResult } from 'app/types'; -import { - apiKeysLoaded, - includeExpiredToggled, - isFetching, - apiKeysMigrationStatusLoaded, - setSearchQuery, -} from './reducers'; - -export function addApiKey(apiKey: ApiKey, openModal: (key: string) => void): ThunkResult { - return async (dispatch) => { - const result = await getBackendSrv().post('/api/auth/keys', apiKey); - dispatch(setSearchQuery('')); - dispatch(loadApiKeys()); - openModal(result.key); - }; -} +import { apiKeysLoaded, includeExpiredToggled, isFetching } from './reducers'; export function loadApiKeys(): ThunkResult { return async (dispatch) => { @@ -53,21 +36,12 @@ export function migrateAll(): ThunkResult { return async (dispatch) => { try { await getBackendSrv().post('/api/serviceaccounts/migrate'); - store.set(API_KEYS_MIGRATION_INFO_STORAGE_KEY, true); } finally { - dispatch(getApiKeysMigrationStatus()); dispatch(loadApiKeys()); } }; } -export function getApiKeysMigrationStatus(): ThunkResult { - return async (dispatch) => { - const result = await getBackendSrv().get('/api/serviceaccounts/migrationstatus'); - dispatch(apiKeysMigrationStatusLoaded(!!result?.migrated)); - }; -} - export function hideApiKeys(): ThunkResult { return async (dispatch) => { await getBackendSrv().post('/api/serviceaccounts/hideApiKeys'); diff --git a/public/app/features/api-keys/state/reducers.ts b/public/app/features/api-keys/state/reducers.ts index 4e51b74ba56..327d18d3abc 100644 --- a/public/app/features/api-keys/state/reducers.ts +++ b/public/app/features/api-keys/state/reducers.ts @@ -8,7 +8,6 @@ export const initialApiKeysState: ApiKeysState = { keys: [], keysIncludingExpired: [], searchQuery: '', - apiKeysMigrated: false, }; const apiKeysSlice = createSlice({ @@ -23,9 +22,6 @@ const apiKeysSlice = createSlice({ : state.includeExpired; return { ...state, hasFetched: true, keys, keysIncludingExpired, includeExpired }; }, - apiKeysMigrationStatusLoaded: (state, action): ApiKeysState => { - return { ...state, apiKeysMigrated: action.payload }; - }, setSearchQuery: (state, action): ApiKeysState => { return { ...state, searchQuery: action.payload }; }, @@ -38,8 +34,7 @@ const apiKeysSlice = createSlice({ }, }); -export const { apiKeysLoaded, includeExpiredToggled, isFetching, setSearchQuery, apiKeysMigrationStatusLoaded } = - apiKeysSlice.actions; +export const { apiKeysLoaded, includeExpiredToggled, isFetching, setSearchQuery } = apiKeysSlice.actions; export const apiKeysReducer = apiKeysSlice.reducer; diff --git a/public/app/features/api-keys/state/selectors.test.ts b/public/app/features/api-keys/state/selectors.test.ts index d96c98f083c..e1ba2f4d0f9 100644 --- a/public/app/features/api-keys/state/selectors.test.ts +++ b/public/app/features/api-keys/state/selectors.test.ts @@ -16,7 +16,6 @@ describe('API Keys selectors', () => { searchQuery: '', hasFetched: true, includeExpired: false, - apiKeysMigrated: false, }; const keyCount = getApiKeysCount(mockState); expect(keyCount).toBe(5); @@ -29,7 +28,6 @@ describe('API Keys selectors', () => { searchQuery: '', hasFetched: true, includeExpired: true, - apiKeysMigrated: false, }; const keyCount = getApiKeysCount(mockState); expect(keyCount).toBe(8); @@ -45,7 +43,6 @@ describe('API Keys selectors', () => { searchQuery: '', hasFetched: true, includeExpired: false, - apiKeysMigrated: false, }; const keys = getApiKeys(mockState); expect(keys).toEqual(mockKeys); @@ -58,7 +55,6 @@ describe('API Keys selectors', () => { searchQuery: '5', hasFetched: true, includeExpired: false, - apiKeysMigrated: false, }; const keys = getApiKeys(mockState); expect(keys.length).toEqual(1); @@ -73,7 +69,6 @@ describe('API Keys selectors', () => { searchQuery: '', hasFetched: true, includeExpired: true, - apiKeysMigrated: false, }; const keys = getApiKeys(mockState); expect(keys).toEqual(mockKeysIncludingExpired); @@ -86,7 +81,6 @@ describe('API Keys selectors', () => { searchQuery: '5', hasFetched: true, includeExpired: true, - apiKeysMigrated: false, }; const keys = getApiKeys(mockState); expect(keys.length).toEqual(1); @@ -102,7 +96,6 @@ describe('API Keys selectors', () => { searchQuery: '', hasFetched: true, includeExpired: true, - apiKeysMigrated: false, }; const includeExpired = getIncludeExpired(mockState); expect(includeExpired).toBe(true); @@ -115,7 +108,6 @@ describe('API Keys selectors', () => { searchQuery: '', hasFetched: true, includeExpired: false, - apiKeysMigrated: false, }; const includeExpired = getIncludeExpired(mockState); expect(includeExpired).toBe(false); @@ -130,7 +122,6 @@ describe('API Keys selectors', () => { searchQuery: '', hasFetched: true, includeExpired: true, - apiKeysMigrated: false, }; const includeExpiredDisabled = getIncludeExpiredDisabled(mockState); expect(includeExpiredDisabled).toBe(true); @@ -143,7 +134,6 @@ describe('API Keys selectors', () => { searchQuery: '', hasFetched: true, includeExpired: false, - apiKeysMigrated: false, }; const includeExpiredDisabled = getIncludeExpired(mockState); expect(includeExpiredDisabled).toBe(false); diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx index bd459659dbe..ee941a3d043 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx @@ -23,9 +23,6 @@ const setup = (propOverrides: Partial) => { const updateServiceAccountMock = jest.fn(); const changeStateFilterMock = jest.fn(); const createServiceAccountTokenMock = jest.fn(); - const getApiKeysMigrationStatusMock = jest.fn(); - const getApiKeysMigrationInfoMock = jest.fn(); - const closeApiKeysMigrationInfoMock = jest.fn(); const props: Props = { isLoading: false, page: 0, @@ -36,8 +33,6 @@ const setup = (propOverrides: Partial) => { showPaging: false, totalPages: 1, serviceAccounts: [], - apiKeysMigrated: false, - showApiKeysMigrationInfo: false, changeQuery: changeQueryMock, fetchACOptions: fetchACOptionsMock, fetchServiceAccounts: fetchServiceAccountsMock, @@ -45,9 +40,6 @@ const setup = (propOverrides: Partial) => { updateServiceAccount: updateServiceAccountMock, changeStateFilter: changeStateFilterMock, createServiceAccountToken: createServiceAccountTokenMock, - getApiKeysMigrationStatus: getApiKeysMigrationStatusMock, - getApiKeysMigrationInfo: getApiKeysMigrationInfoMock, - closeApiKeysMigrationInfo: closeApiKeysMigrationInfoMock, }; Object.assign(props, propOverrides); diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx index 9cd3b81bcce..8f04334e591 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { GrafanaTheme2, OrgRole } from '@grafana/data'; -import { Alert, ConfirmModal, FilterInput, Icon, LinkButton, RadioButtonGroup, Tooltip, useStyles2 } from '@grafana/ui'; +import { ConfirmModal, FilterInput, Icon, LinkButton, RadioButtonGroup, Tooltip, useStyles2 } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import { Page } from 'app/core/components/Page/Page'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; @@ -21,9 +21,6 @@ import { updateServiceAccount, changeStateFilter, createServiceAccountToken, - getApiKeysMigrationStatus, - getApiKeysMigrationInfo, - closeApiKeysMigrationInfo, } from './state/actions'; interface OwnProps {} @@ -44,9 +41,6 @@ const mapDispatchToProps = { updateServiceAccount, changeStateFilter, createServiceAccountToken, - getApiKeysMigrationStatus, - getApiKeysMigrationInfo, - closeApiKeysMigrationInfo, }; const connector = connect(mapStateToProps, mapDispatchToProps); @@ -57,8 +51,6 @@ export const ServiceAccountsListPageUnconnected = ({ roleOptions, query, serviceAccountStateFilter, - apiKeysMigrated, - showApiKeysMigrationInfo, changeQuery, fetchACOptions, fetchServiceAccounts, @@ -66,9 +58,6 @@ export const ServiceAccountsListPageUnconnected = ({ updateServiceAccount, changeStateFilter, createServiceAccountToken, - getApiKeysMigrationStatus, - getApiKeysMigrationInfo, - closeApiKeysMigrationInfo, }: Props): JSX.Element => { const styles = useStyles2(getStyles); const [isAddModalOpen, setIsAddModalOpen] = useState(false); @@ -79,12 +68,10 @@ export const ServiceAccountsListPageUnconnected = ({ useEffect(() => { fetchServiceAccounts({ withLoadingIndicator: true }); - getApiKeysMigrationStatus(); - getApiKeysMigrationInfo(); if (contextSrv.licensedAccessControlEnabled()) { fetchACOptions(); } - }, [fetchACOptions, fetchServiceAccounts, getApiKeysMigrationStatus, getApiKeysMigrationInfo]); + }, [fetchACOptions, fetchServiceAccounts]); const noServiceAccountsCreated = serviceAccounts.length === 0 && serviceAccountStateFilter === ServiceAccountStateFilter.All && !query; @@ -160,10 +147,6 @@ export const ServiceAccountsListPageUnconnected = ({ setCurrentServiceAccount(null); }; - const onMigrationInfoClose = () => { - closeApiKeysMigrationInfo(); - }; - const docsLink = ( - {apiKeysMigrated && showApiKeysMigrationInfo && ( - - )}

Service accounts

diff --git a/public/app/features/serviceaccounts/constants.ts b/public/app/features/serviceaccounts/constants.ts deleted file mode 100644 index 40ed3e20666..00000000000 --- a/public/app/features/serviceaccounts/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const API_KEYS_MIGRATION_INFO_STORAGE_KEY = 'grafana.serviceaccounts.showApiKeysMigrationInfo'; diff --git a/public/app/features/serviceaccounts/state/actions.ts b/public/app/features/serviceaccounts/state/actions.ts index 74e280f0c5e..7409beccba5 100644 --- a/public/app/features/serviceaccounts/state/actions.ts +++ b/public/app/features/serviceaccounts/state/actions.ts @@ -3,11 +3,9 @@ import { debounce } from 'lodash'; import { getBackendSrv } from '@grafana/runtime'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; import { contextSrv } from 'app/core/services/context_srv'; -import store from 'app/core/store'; import { AccessControlAction, ServiceAccountDTO, ServiceAccountStateFilter, ThunkResult } from 'app/types'; import { ServiceAccountToken } from '../components/CreateTokenModal'; -import { API_KEYS_MIGRATION_INFO_STORAGE_KEY } from '../constants'; import { acOptionsLoaded, @@ -16,9 +14,7 @@ import { serviceAccountsFetchBegin, serviceAccountsFetched, serviceAccountsFetchEnd, - apiKeysMigrationStatusLoaded, stateFilterChanged, - showApiKeysMigrationInfoLoaded, } from './reducers'; const BASE_URL = `/api/serviceaccounts`; @@ -36,15 +32,6 @@ export function fetchACOptions(): ThunkResult { }; } -export function getApiKeysMigrationStatus(): ThunkResult { - return async (dispatch) => { - if (contextSrv.hasPermission(AccessControlAction.ServiceAccountsRead)) { - const result = await getBackendSrv().get('/api/serviceaccounts/migrationstatus'); - dispatch(apiKeysMigrationStatusLoaded(!!result?.migrated)); - } - }; -} - interface FetchServiceAccountsParams { withLoadingIndicator: boolean; } @@ -138,17 +125,3 @@ export function changePage(page: number): ThunkResult { dispatch(fetchServiceAccounts()); }; } - -export function getApiKeysMigrationInfo(): ThunkResult { - return async (dispatch) => { - const showApiKeysMigrationInfo = store.getBool(API_KEYS_MIGRATION_INFO_STORAGE_KEY, false); - dispatch(showApiKeysMigrationInfoLoaded(showApiKeysMigrationInfo)); - }; -} - -export function closeApiKeysMigrationInfo(): ThunkResult { - return async (dispatch) => { - store.set(API_KEYS_MIGRATION_INFO_STORAGE_KEY, false); - dispatch(getApiKeysMigrationInfo()); - }; -} diff --git a/public/app/features/serviceaccounts/state/reducers.ts b/public/app/features/serviceaccounts/state/reducers.ts index b87855e1f0c..8ff78f79ae4 100644 --- a/public/app/features/serviceaccounts/state/reducers.ts +++ b/public/app/features/serviceaccounts/state/reducers.ts @@ -50,8 +50,6 @@ export const initialStateList: ServiceAccountsState = { totalPages: 1, showPaging: false, serviceAccountStateFilter: ServiceAccountStateFilter.All, - apiKeysMigrated: false, - showApiKeysMigrationInfo: false, }; interface ServiceAccountsFetched { @@ -87,12 +85,6 @@ const serviceAccountsSlice = createSlice({ acOptionsLoaded: (state, action: PayloadAction): ServiceAccountsState => { return { ...state, roleOptions: action.payload }; }, - apiKeysMigrationStatusLoaded: (state, action): ServiceAccountsState => { - return { ...state, apiKeysMigrated: action.payload }; - }, - showApiKeysMigrationInfoLoaded: (state, action): ServiceAccountsState => { - return { ...state, showApiKeysMigrationInfo: action.payload }; - }, queryChanged: (state, action: PayloadAction) => { return { ...state, @@ -117,8 +109,6 @@ export const { serviceAccountsFetchEnd, serviceAccountsFetched, acOptionsLoaded, - apiKeysMigrationStatusLoaded, - showApiKeysMigrationInfoLoaded, pageChanged, stateFilterChanged, queryChanged, diff --git a/public/app/types/apiKeys.ts b/public/app/types/apiKeys.ts index c8c06f4a0db..241986acf15 100644 --- a/public/app/types/apiKeys.ts +++ b/public/app/types/apiKeys.ts @@ -15,17 +15,10 @@ export interface ApiKey extends WithAccessControlMetadata { lastUsedAt?: string; } -export interface NewApiKey { - name: string; - role: OrgRole; - secondsToLive: string; -} - export interface ApiKeysState { includeExpired: boolean; keys: ApiKey[]; keysIncludingExpired: ApiKey[]; searchQuery: string; hasFetched: boolean; - apiKeysMigrated: boolean; } diff --git a/public/app/types/serviceaccount.ts b/public/app/types/serviceaccount.ts index 9fc051ceca6..cad4fea10d9 100644 --- a/public/app/types/serviceaccount.ts +++ b/public/app/types/serviceaccount.ts @@ -65,8 +65,6 @@ export interface ServiceAccountsState { serviceAccounts: ServiceAccountDTO[]; isLoading: boolean; roleOptions: Role[]; - apiKeysMigrated: boolean; - showApiKeysMigrationInfo: boolean; // search / filtering query: string;