mirror of
https://github.com/grafana/grafana.git
synced 2026-09-05 04:40:13 -05:00
Service accounts: Remove Add API keys buttons and remove one state of migrating for API keys tab (#63411)
* add: hide apikeys tab on start * make use of store method * added hiding of apikeys tab for new org creation * missing err check * removed unused files * implemennted fake to make tests run * move check for globalHideApikeys from org to admin * refactor to remove the fake * removed unused method calls for interface * Update pkg/services/serviceaccounts/manager/service.go Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com> * Update pkg/services/serviceaccounts/manager/service.go Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com> * remove the checkglobal method * removed duplicate global set const * add count of apikeys for performance * remove apikeys adding in UI * added back deleted file * added comment on component * changed wording and copy for hiding and migrating service accounts * refactor: remove migrationstatus in front/backend This removes the migrationstatus state from the UI in favor of only looking at the number of API keys to determine what to show to the user. This simplifies the logic and makes less calls to the backend with each page load. This was called both on the API keys page and the Service accounts page. - removes the state of migrationstatus from the UI - removes the backend call - removes the backend endpoint for migrationstatus * Update pkg/services/apikey/apikeyimpl/xorm_store.go Co-authored-by: Karl Persson <kalle.persson@grafana.com> * changes the contet to also be primary * change id of version for footer component --------- Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com> Co-authored-by: Karl Persson <kalle.persson@grafana.com>
This commit is contained in:
co-authored by
Karl Persson
Alexander Zobnin
parent
8e9ccfc66e
commit
9d6ab92e39
@@ -2795,9 +2795,6 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Do not use any type assertions.", "5"],
|
[0, 0, 0, "Do not use any type assertions.", "5"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
|
[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": [
|
"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.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
type Service interface {
|
type Service interface {
|
||||||
GetAPIKeys(ctx context.Context, query *GetApiKeysQuery) error
|
GetAPIKeys(ctx context.Context, query *GetApiKeysQuery) error
|
||||||
GetAllAPIKeys(ctx context.Context, orgID int64) ([]*APIKey, error)
|
GetAllAPIKeys(ctx context.Context, orgID int64) ([]*APIKey, error)
|
||||||
|
CountAPIKeys(ctx context.Context, orgID int64) (int64, error)
|
||||||
DeleteApiKey(ctx context.Context, cmd *DeleteCommand) error
|
DeleteApiKey(ctx context.Context, cmd *DeleteCommand) error
|
||||||
AddAPIKey(ctx context.Context, cmd *AddCommand) error
|
AddAPIKey(ctx context.Context, cmd *AddCommand) error
|
||||||
GetApiKeyById(ctx context.Context, query *GetByIDQuery) error
|
GetApiKeyById(ctx context.Context, query *GetByIDQuery) error
|
||||||
|
|||||||
@@ -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 {
|
func (s *Service) UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64) error {
|
||||||
return s.store.UpdateAPIKeyLastUsedDate(ctx, tokenID)
|
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) {
|
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
|
||||||
limits := "a.Map{}
|
limits := "a.Map{}
|
||||||
|
|||||||
@@ -63,6 +63,18 @@ func (ss *sqlxStore) GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey.
|
|||||||
return result, err
|
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 {
|
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)
|
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 {
|
if err != nil {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
type store interface {
|
type store interface {
|
||||||
GetAPIKeys(ctx context.Context, query *apikey.GetApiKeysQuery) error
|
GetAPIKeys(ctx context.Context, query *apikey.GetApiKeysQuery) error
|
||||||
GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey.APIKey, 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
|
DeleteApiKey(ctx context.Context, cmd *apikey.DeleteCommand) error
|
||||||
AddAPIKey(ctx context.Context, cmd *apikey.AddCommand) error
|
AddAPIKey(ctx context.Context, cmd *apikey.AddCommand) error
|
||||||
GetApiKeyById(ctx context.Context, query *apikey.GetByIDQuery) error
|
GetApiKeyById(ctx context.Context, query *apikey.GetByIDQuery) error
|
||||||
|
|||||||
@@ -65,6 +65,25 @@ func (ss *sqlStore) GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey.A
|
|||||||
return result, err
|
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 {
|
func (ss *sqlStore) DeleteApiKey(ctx context.Context, cmd *apikey.DeleteCommand) error {
|
||||||
return ss.db.WithDbSession(ctx, func(sess *db.Session) 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"
|
rawSQL := "DELETE FROM api_key WHERE id=? and org_id=? and service_account_id IS NULL"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
ExpectedError error
|
ExpectedError error
|
||||||
|
ExpectedCount int64
|
||||||
ExpectedAPIKeys []*apikey.APIKey
|
ExpectedAPIKeys []*apikey.APIKey
|
||||||
ExpectedAPIKey *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) {
|
func (s *Service) GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey.APIKey, error) {
|
||||||
return s.ExpectedAPIKeys, s.ExpectedError
|
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 {
|
func (s *Service) GetApiKeyById(ctx context.Context, query *apikey.GetByIDQuery) error {
|
||||||
query.Result = s.ExpectedAPIKey
|
query.Result = s.ExpectedAPIKey
|
||||||
return s.ExpectedError
|
return s.ExpectedError
|
||||||
|
|||||||
@@ -80,12 +80,13 @@ func (s *ServiceImpl) getOrgAdminNode(c *contextmodel.ReqContext) (*navtree.NavL
|
|||||||
}
|
}
|
||||||
|
|
||||||
hideApiKeys, _, _ := s.kvStore.Get(c.Req.Context(), c.OrgID, "serviceaccounts", "hideApiKeys")
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if hasAccess(ac.ReqOrgAdmin, ac.ApiKeyAccessEvaluator) && !apiKeysHidden {
|
||||||
configNodes = append(configNodes, &navtree.NavLink{
|
configNodes = append(configNodes, &navtree.NavLink{
|
||||||
Text: "API keys",
|
Text: "API keys",
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ type service interface {
|
|||||||
SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error)
|
SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error)
|
||||||
ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error)
|
ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error)
|
||||||
DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID 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
|
HideApiKeysTab(ctx context.Context, orgID int64) error
|
||||||
MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) error
|
MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) error
|
||||||
MigrateApiKey(ctx context.Context, orgID int64, keyId 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))
|
accesscontrol.EvalPermission(serviceaccounts.ActionWrite, serviceaccounts.ScopeID)), routing.Wrap(api.CreateToken))
|
||||||
serviceAccountsRoute.Delete("/:serviceAccountId/tokens/:tokenId", auth(middleware.ReqOrgAdmin,
|
serviceAccountsRoute.Delete("/:serviceAccountId/tokens/:tokenId", auth(middleware.ReqOrgAdmin,
|
||||||
accesscontrol.EvalPermission(serviceaccounts.ActionWrite, serviceaccounts.ScopeID)), routing.Wrap(api.DeleteToken))
|
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,
|
serviceAccountsRoute.Post("/hideApiKeys", auth(middleware.ReqOrgAdmin,
|
||||||
accesscontrol.EvalPermission(serviceaccounts.ActionCreate)), routing.Wrap(api.HideApiKeysTab))
|
accesscontrol.EvalPermission(serviceaccounts.ActionCreate)), routing.Wrap(api.HideApiKeysTab))
|
||||||
serviceAccountsRoute.Post("/migrate", auth(middleware.ReqOrgAdmin,
|
serviceAccountsRoute.Post("/migrate", auth(middleware.ReqOrgAdmin,
|
||||||
@@ -364,15 +361,6 @@ func (api *ServiceAccountsAPI) SearchOrgServiceAccountsWithPaging(c *contextmode
|
|||||||
return response.JSON(http.StatusOK, serviceAccountSearch)
|
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
|
// POST /api/serviceaccounts/hideapikeys
|
||||||
func (api *ServiceAccountsAPI) HideApiKeysTab(ctx *contextmodel.ReqContext) response.Response {
|
func (api *ServiceAccountsAPI) HideApiKeysTab(ctx *contextmodel.ReqContext) response.Response {
|
||||||
if err := api.service.HideApiKeysTab(ctx.Req.Context(), ctx.OrgID); err != nil {
|
if err := api.service.HideApiKeysTab(ctx.Req.Context(), ctx.OrgID); err != nil {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
server := setupTests(t, func(a *ServiceAccountsAPI) {
|
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))
|
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)}})
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
server := setupTests(t, func(a *ServiceAccountsAPI) {
|
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))
|
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)}})
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
server := setupTests(t, func(a *ServiceAccountsAPI) {
|
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))
|
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()
|
cfg := setting.NewCfg()
|
||||||
api := &ServiceAccountsAPI{
|
api := &ServiceAccountsAPI{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
service: &fakeService{},
|
service: &fakeServiceAccountService{},
|
||||||
accesscontrolService: &actest.FakeService{},
|
accesscontrolService: &actest.FakeService{},
|
||||||
accesscontrol: acimpl.ProvideAccessControl(cfg),
|
accesscontrol: acimpl.ProvideAccessControl(cfg),
|
||||||
RouterRegister: routing.NewRouteRegister(),
|
RouterRegister: routing.NewRouteRegister(),
|
||||||
@@ -255,9 +255,9 @@ func setupTests(t *testing.T, opts ...func(a *ServiceAccountsAPI)) *webtest.Serv
|
|||||||
return webtest.NewServer(t, api.RouterRegister)
|
return webtest.NewServer(t, api.RouterRegister)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ service = new(fakeService)
|
var _ service = new(fakeServiceAccountService)
|
||||||
|
|
||||||
type fakeService struct {
|
type fakeServiceAccountService struct {
|
||||||
service
|
service
|
||||||
ExpectedErr error
|
ExpectedErr error
|
||||||
ExpectedAPIKey *apikey.APIKey
|
ExpectedAPIKey *apikey.APIKey
|
||||||
@@ -266,30 +266,30 @@ type fakeService struct {
|
|||||||
ExpectedServiceAccountProfile *serviceaccounts.ServiceAccountProfileDTO
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
return f.ExpectedErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func TestServiceAccountsAPI_ListTokens(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
server := setupTests(t, func(a *ServiceAccountsAPI) {
|
server := setupTests(t, func(a *ServiceAccountsAPI) {
|
||||||
a.service = &fakeService{}
|
a.service = &fakeServiceAccountService{}
|
||||||
})
|
})
|
||||||
req := server.NewGetRequest(fmt.Sprintf("/api/serviceaccounts/%d/tokens", tt.id))
|
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)}})
|
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) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
server := setupTests(t, func(a *ServiceAccountsAPI) {
|
server := setupTests(t, func(a *ServiceAccountsAPI) {
|
||||||
a.cfg.ApiKeyMaxSecondsToLive = tt.tokenTTL
|
a.cfg.ApiKeyMaxSecondsToLive = tt.tokenTTL
|
||||||
a.service = &fakeService{
|
a.service = &fakeServiceAccountService{
|
||||||
ExpectedErr: tt.expectedErr,
|
ExpectedErr: tt.expectedErr,
|
||||||
ExpectedAPIKey: tt.expectedAPIKey,
|
ExpectedAPIKey: tt.expectedAPIKey,
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@ func TestServiceAccountsAPI_DeleteToken(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
server := setupTests(t, func(a *ServiceAccountsAPI) {
|
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)
|
req := server.NewRequest(http.MethodDelete, fmt.Sprintf("/api/serviceaccounts/%d/tokens/%d", tt.saID, tt.apikeyID), nil)
|
||||||
|
|||||||
@@ -388,22 +388,6 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context,
|
|||||||
return searchResult, nil
|
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 {
|
func (s *ServiceAccountsStoreImpl) HideApiKeysTab(ctx context.Context, orgId int64) error {
|
||||||
if err := s.kvStore.Set(ctx, orgId, "serviceaccounts", "hideApiKeys", "1"); err != nil {
|
if err := s.kvStore.Set(ctx, orgId, "serviceaccounts", "hideApiKeys", "1"); err != nil {
|
||||||
s.log.Error("Failed to hide API keys tab", err)
|
s.log.Error("Failed to hide API keys tab", err)
|
||||||
|
|||||||
@@ -226,13 +226,6 @@ func (sa *ServiceAccountsService) DeleteServiceAccountToken(ctx context.Context,
|
|||||||
return sa.store.DeleteServiceAccountToken(ctx, orgID, serviceAccountID, tokenID)
|
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 {
|
func (sa *ServiceAccountsService) HideApiKeysTab(ctx context.Context, orgID int64) error {
|
||||||
if err := validOrgID(orgID); err != nil {
|
if err := validOrgID(orgID); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ type FakeServiceAccountStore struct {
|
|||||||
ExpectedServiceAccountDTO *serviceaccounts.ServiceAccountDTO
|
ExpectedServiceAccountDTO *serviceaccounts.ServiceAccountDTO
|
||||||
ExpectedServiceAccountProfileDTO *serviceaccounts.ServiceAccountProfileDTO
|
ExpectedServiceAccountProfileDTO *serviceaccounts.ServiceAccountProfileDTO
|
||||||
ExpectedSearchServiceAccountQueryResult *serviceaccounts.SearchOrgServiceAccountsResult
|
ExpectedSearchServiceAccountQueryResult *serviceaccounts.SearchOrgServiceAccountsResult
|
||||||
ExpectedServiceAccountMigrationStatus *serviceaccounts.APIKeysMigrationStatus
|
|
||||||
ExpectedStats *serviceaccounts.Stats
|
ExpectedStats *serviceaccounts.Stats
|
||||||
ExpectedAPIKeys []apikey.APIKey
|
ExpectedAPIKeys []apikey.APIKey
|
||||||
ExpectedAPIKey *apikey.APIKey
|
ExpectedAPIKey *apikey.APIKey
|
||||||
|
ExpectedBoolean bool
|
||||||
ExpectedError error
|
ExpectedError error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,11 +59,6 @@ func (f *FakeServiceAccountStore) DeleteServiceAccount(ctx context.Context, orgI
|
|||||||
return f.ExpectedError
|
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.
|
// HideApiKeysTab is a fake hiding the api keys tab.
|
||||||
func (f *FakeServiceAccountStore) HideApiKeysTab(ctx context.Context, orgID int64) error {
|
func (f *FakeServiceAccountStore) HideApiKeysTab(ctx context.Context, orgID int64) error {
|
||||||
return f.ExpectedError
|
return f.ExpectedError
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ type store interface {
|
|||||||
RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error)
|
RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error)
|
||||||
RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error)
|
RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error)
|
||||||
DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID 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
|
HideApiKeysTab(ctx context.Context, orgID int64) error
|
||||||
MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) error
|
MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) error
|
||||||
MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error
|
MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error
|
||||||
|
|||||||
@@ -130,10 +130,6 @@ type ServiceAccountProfileDTO struct {
|
|||||||
|
|
||||||
type ServiceAccountFilter string // used for filtering
|
type ServiceAccountFilter string // used for filtering
|
||||||
|
|
||||||
type APIKeysMigrationStatus struct {
|
|
||||||
Migrated bool `json:"migrated"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FilterOnlyExpiredTokens ServiceAccountFilter = "expiredTokens"
|
FilterOnlyExpiredTokens ServiceAccountFilter = "expiredTokens"
|
||||||
FilterOnlyDisabled ServiceAccountFilter = "disabled"
|
FilterOnlyDisabled ServiceAccountFilter = "disabled"
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function getVersionLinks(): FooterLink[] {
|
|||||||
|
|
||||||
links.push({
|
links.push({
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
id: 'version',
|
id: 'license',
|
||||||
text: `${buildInfo.edition}${stateInfo}`,
|
text: `${buildInfo.edition}${stateInfo}`,
|
||||||
url: licenseInfo.licenseUrl,
|
url: licenseInfo.licenseUrl,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,29 +6,31 @@ import { Alert, ConfirmModal, useStyles2, Button } from '@grafana/ui';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onHideApiKeys: () => void;
|
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 [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert title="API keys were migrated to Grafana service accounts. This tab is deprecated." severity="info">
|
<Alert title="If you see any API keys please migrate them to Service account tokens." severity="info">
|
||||||
<div className={styles.text}>
|
<div className={styles.text}>
|
||||||
We have migrated API keys into Grafana service accounts. All API keys are safe and continue working as they used
|
Migrated API keys are safe and continue working as they used to. You can find them inside the respective service
|
||||||
to, you can find them inside the respective service account.
|
account.
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.actionRow}>
|
<div className={styles.actionRow}>
|
||||||
<Button className={styles.actionButton} onClick={() => setIsModalOpen(true)}>
|
<Button className={styles.actionButton} onClick={() => setIsModalOpen(true)} disabled={apikeys !== 0}>
|
||||||
Hide API keys page forever
|
Hide API keys page
|
||||||
</Button>
|
</Button>
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
title={'Hide API Keys page forever'}
|
title={'Hide API Keys page'}
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
body={'Are you sure you want to hide API keys page forever and use service accounts from now on?'}
|
body={'Did you want to hide the API keys page?'}
|
||||||
confirmText={'Yes, hide API keys page.'}
|
confirmText={'Yes, hide API keys page.'}
|
||||||
onConfirm={onHideApiKeys}
|
onConfirm={onHideApiKeys}
|
||||||
onDismiss={() => setIsModalOpen(false)}
|
onDismiss={() => setIsModalOpen(false)}
|
||||||
|
confirmButtonVariant="primary"
|
||||||
/>
|
/>
|
||||||
<a href="org/serviceaccounts">View service accounts page</a>
|
<a href="org/serviceaccounts">View service accounts page</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,23 +1,19 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { Button, FilterInput } from '@grafana/ui';
|
import { FilterInput } from '@grafana/ui';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
onAddClick: () => void;
|
|
||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ApiKeysActionBar: FC<Props> = ({ searchQuery, disabled, onAddClick, onSearchChange }) => {
|
export const ApiKeysActionBar: FC<Props> = ({ searchQuery, disabled, onSearchChange }) => {
|
||||||
return (
|
return (
|
||||||
<div className="page-action-bar">
|
<div className="page-action-bar">
|
||||||
<div className="gf-form gf-form--grow">
|
<div className="gf-form gf-form--grow">
|
||||||
<FilterInput placeholder="Search keys" value={searchQuery} onChange={onSearchChange} />
|
<FilterInput placeholder="Search keys" value={searchQuery} onChange={onSearchChange} />
|
||||||
</div>
|
</div>
|
||||||
<Button className="pull-right" onClick={onAddClick} disabled={disabled}>
|
|
||||||
Add API key
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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(<ApiKeysAddedModal {...props} />)).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays the apiKey in a readOnly input', () => {
|
|
||||||
render(<ApiKeysAddedModal {...props} />);
|
|
||||||
const input = screen.getByRole('textbox');
|
|
||||||
expect(input).toHaveValue(props.apiKey);
|
|
||||||
expect(input).toHaveAttribute('readonly');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a `Copy to clipboard` button', () => {
|
|
||||||
render(<ApiKeysAddedModal {...props} />);
|
|
||||||
expect(screen.getByRole('button', { name: 'Copy' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays the correct curl path', () => {
|
|
||||||
render(<ApiKeysAddedModal {...props} />);
|
|
||||||
expect(
|
|
||||||
screen.getByText('curl -H "Authorization: Bearer myApiKey" test/path/api/dashboards/home')
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls onDismiss when the modal is closed', () => {
|
|
||||||
render(<ApiKeysAddedModal {...props} />);
|
|
||||||
screen.getByRole('button', { name: 'Close dialogue' }).click();
|
|
||||||
expect(props.onDismiss).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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 (
|
|
||||||
<Modal title="API Key Created" onDismiss={onDismiss} onClickBackdrop={onDismiss} isOpen>
|
|
||||||
<Field label="Key">
|
|
||||||
<Input
|
|
||||||
id="Key"
|
|
||||||
value={apiKey}
|
|
||||||
readOnly
|
|
||||||
addonAfter={
|
|
||||||
<ClipboardButton icon="copy" variant="primary" getText={getClipboardText}>
|
|
||||||
Copy
|
|
||||||
</ClipboardButton>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Alert severity="info" title="You will only be able to view this key here once!">
|
|
||||||
It is not stored in this form, so be sure to copy it now.
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<p className="text-muted">You can authenticate a request using the Authorization HTTP header, example:</p>
|
|
||||||
<pre className={styles.small}>
|
|
||||||
curl -H "Authorization: Bearer {apiKey}" {rootPath}/api/dashboards/home
|
|
||||||
</pre>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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};
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ApiKeysController: FC<Props> = ({ children }) => {
|
export const ApiKeysController: FC<Props> = ({ children }) => {
|
||||||
|
// FIXME(eleijonmarck): could not remove state from this component
|
||||||
|
// as component cannot render properly without it
|
||||||
const [isAdding, setIsAdding] = useState<boolean>(false);
|
const [isAdding, setIsAdding] = useState<boolean>(false);
|
||||||
const toggleIsAdding = useCallback(() => {
|
const toggleIsAdding = useCallback(() => {
|
||||||
setIsAdding(!isAdding);
|
setIsAdding(!isAdding);
|
||||||
|
|||||||
@@ -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<SelectableValue<OrgRole>> = 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<Props> = ({ show, onClose, onKeyAdded, disabled }) => {
|
|
||||||
const [name, setName] = useState<string>('');
|
|
||||||
const [role, setRole] = useState<OrgRole>(OrgRole.Viewer);
|
|
||||||
const [secondsToLive, setSecondsToLive] = useState<string>('');
|
|
||||||
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<HTMLInputElement>) => {
|
|
||||||
setName(event.currentTarget.value);
|
|
||||||
};
|
|
||||||
const onRoleChange = (role: SelectableValue<OrgRole>) => {
|
|
||||||
setRole(role.value!);
|
|
||||||
};
|
|
||||||
const onSecondsToLiveChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setSecondsToLive(event.currentTarget.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SlideDown in={show}>
|
|
||||||
<div className="gf-form-inline cta-form">
|
|
||||||
<CloseButton onClick={onClose} />
|
|
||||||
<form className="gf-form-group" onSubmit={onSubmit}>
|
|
||||||
<h5>Add API Key</h5>
|
|
||||||
<div className="gf-form-inline">
|
|
||||||
<div className="gf-form max-width-21">
|
|
||||||
<span className="gf-form-label">Key name</span>
|
|
||||||
<Input type="text" className="gf-form-input" value={name} placeholder="Name" onChange={onNameChange} />
|
|
||||||
</div>
|
|
||||||
<div className="gf-form">
|
|
||||||
<InlineField label="Role">
|
|
||||||
<Select inputId="role-select" value={role} onChange={onRoleChange} options={ROLE_OPTIONS} />
|
|
||||||
</InlineField>
|
|
||||||
</div>
|
|
||||||
<div className="gf-form max-width-21">
|
|
||||||
<InlineField tooltip={tooltipText} label="Time to live">
|
|
||||||
<Input
|
|
||||||
id="time-to-live-input"
|
|
||||||
type="text"
|
|
||||||
placeholder="1d"
|
|
||||||
validationEvents={timeRangeValidationEvents}
|
|
||||||
value={secondsToLive}
|
|
||||||
onChange={onSecondsToLiveChange}
|
|
||||||
/>
|
|
||||||
</InlineField>
|
|
||||||
</div>
|
|
||||||
<div className="gf-form">
|
|
||||||
<Button type="submit" disabled={disabled}>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</SlideDown>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -3,7 +3,6 @@ import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event'
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TestProvider } from 'test/helpers/TestProvider';
|
import { TestProvider } from 'test/helpers/TestProvider';
|
||||||
|
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
|
||||||
import { ApiKey, OrgRole } from 'app/types';
|
import { ApiKey, OrgRole } from 'app/types';
|
||||||
|
|
||||||
import { mockToolkitActionCreator } from '../../../test/core/redux/mocks';
|
import { mockToolkitActionCreator } from '../../../test/core/redux/mocks';
|
||||||
@@ -30,7 +29,6 @@ const setup = (propOverrides: Partial<Props>) => {
|
|||||||
const migrateAllMock = jest.fn();
|
const migrateAllMock = jest.fn();
|
||||||
const toggleIncludeExpiredMock = jest.fn();
|
const toggleIncludeExpiredMock = jest.fn();
|
||||||
const setSearchQueryMock = mockToolkitActionCreator(setSearchQuery);
|
const setSearchQueryMock = mockToolkitActionCreator(setSearchQuery);
|
||||||
const getApiKeysMigrationStatusMock = jest.fn();
|
|
||||||
const hideApiKeysMock = jest.fn();
|
const hideApiKeysMock = jest.fn();
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
apiKeys: [] as ApiKey[],
|
apiKeys: [] as ApiKey[],
|
||||||
@@ -39,8 +37,6 @@ const setup = (propOverrides: Partial<Props>) => {
|
|||||||
loadApiKeys: loadApiKeysMock,
|
loadApiKeys: loadApiKeysMock,
|
||||||
deleteApiKey: deleteApiKeyMock,
|
deleteApiKey: deleteApiKeyMock,
|
||||||
setSearchQuery: setSearchQueryMock,
|
setSearchQuery: setSearchQueryMock,
|
||||||
addApiKey: addApiKeyMock,
|
|
||||||
getApiKeysMigrationStatus: getApiKeysMigrationStatusMock,
|
|
||||||
migrateApiKey: migrateApiKeyMock,
|
migrateApiKey: migrateApiKeyMock,
|
||||||
migrateAll: migrateAllMock,
|
migrateAll: migrateAllMock,
|
||||||
hideApiKeys: hideApiKeysMock,
|
hideApiKeys: hideApiKeysMock,
|
||||||
@@ -50,7 +46,6 @@ const setup = (propOverrides: Partial<Props>) => {
|
|||||||
includeExpiredDisabled: false,
|
includeExpiredDisabled: false,
|
||||||
toggleIncludeExpired: toggleIncludeExpiredMock,
|
toggleIncludeExpired: toggleIncludeExpiredMock,
|
||||||
canCreate: true,
|
canCreate: true,
|
||||||
apiKeysMigrated: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
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', () => {
|
describe('when there are API keys', () => {
|
||||||
it('then it should render API keys table', async () => {
|
it('then it should render API keys table', async () => {
|
||||||
const apiKeys = [
|
const apiKeys = [
|
||||||
@@ -165,66 +153,9 @@ describe('ApiKeysPage', () => {
|
|||||||
expect(deleteApiKeyMock).toHaveBeenCalledWith(2);
|
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() {
|
async function toggleShowExpired() {
|
||||||
expect(screen.queryByLabelText(/include expired keys/i)).toBeInTheDocument();
|
expect(screen.queryByLabelText(/include expired keys/i)).toBeInTheDocument();
|
||||||
await userEvent.click(screen.getByLabelText(/include expired keys/i));
|
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());
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,33 +2,24 @@ import React, { PureComponent } from 'react';
|
|||||||
import { connect, ConnectedProps } from 'react-redux';
|
import { connect, ConnectedProps } from 'react-redux';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { rangeUtil } from '@grafana/data';
|
|
||||||
import { locationService } from '@grafana/runtime';
|
import { locationService } from '@grafana/runtime';
|
||||||
import { InlineField, InlineSwitch, VerticalGroup } from '@grafana/ui';
|
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 { Page } from 'app/core/components/Page/Page';
|
||||||
import config from 'app/core/config';
|
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||||
import { AccessControlAction, ApiKey, NewApiKey, StoreState } from 'app/types';
|
import { AccessControlAction, ApiKey, StoreState } from 'app/types';
|
||||||
import { ShowModalReactEvent } from 'app/types/events';
|
|
||||||
|
|
||||||
import { APIKeysMigratedCard } from './APIKeysMigratedCard';
|
import { APIKeysMigratedCard } from './APIKeysMigratedCard';
|
||||||
import { ApiKeysActionBar } from './ApiKeysActionBar';
|
import { ApiKeysActionBar } from './ApiKeysActionBar';
|
||||||
import { ApiKeysAddedModal } from './ApiKeysAddedModal';
|
|
||||||
import { ApiKeysController } from './ApiKeysController';
|
import { ApiKeysController } from './ApiKeysController';
|
||||||
import { ApiKeysForm } from './ApiKeysForm';
|
|
||||||
import { ApiKeysTable } from './ApiKeysTable';
|
import { ApiKeysTable } from './ApiKeysTable';
|
||||||
import { MigrateToServiceAccountsCard } from './MigrateToServiceAccountsCard';
|
import { MigrateToServiceAccountsCard } from './MigrateToServiceAccountsCard';
|
||||||
import {
|
import {
|
||||||
addApiKey,
|
|
||||||
deleteApiKey,
|
deleteApiKey,
|
||||||
migrateApiKey,
|
migrateApiKey,
|
||||||
migrateAll,
|
migrateAll,
|
||||||
loadApiKeys,
|
loadApiKeys,
|
||||||
toggleIncludeExpired,
|
toggleIncludeExpired,
|
||||||
getApiKeysMigrationStatus,
|
|
||||||
hideApiKeys,
|
hideApiKeys,
|
||||||
} from './state/actions';
|
} from './state/actions';
|
||||||
import { setSearchQuery } from './state/reducers';
|
import { setSearchQuery } from './state/reducers';
|
||||||
@@ -46,7 +37,6 @@ function mapStateToProps(state: StoreState) {
|
|||||||
includeExpired: getIncludeExpired(state.apiKeys),
|
includeExpired: getIncludeExpired(state.apiKeys),
|
||||||
includeExpiredDisabled: getIncludeExpiredDisabled(state.apiKeys),
|
includeExpiredDisabled: getIncludeExpiredDisabled(state.apiKeys),
|
||||||
canCreate: canCreate,
|
canCreate: canCreate,
|
||||||
apiKeysMigrated: state.apiKeys.apiKeysMigrated,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,8 +51,6 @@ const mapDispatchToProps = {
|
|||||||
migrateAll,
|
migrateAll,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
toggleIncludeExpired,
|
toggleIncludeExpired,
|
||||||
addApiKey,
|
|
||||||
getApiKeysMigrationStatus,
|
|
||||||
hideApiKeys,
|
hideApiKeys,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,7 +71,6 @@ export class ApiKeysPageUnconnected extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.fetchApiKeys();
|
this.fetchApiKeys();
|
||||||
this.props.getApiKeysMigrationStatus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchApiKeys() {
|
async fetchApiKeys() {
|
||||||
@@ -110,40 +97,6 @@ export class ApiKeysPageUnconnected extends PureComponent<Props, State> {
|
|||||||
this.props.toggleIncludeExpired();
|
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 () => {
|
onHideApiKeys = async () => {
|
||||||
try {
|
try {
|
||||||
await this.props.hideApiKeys();
|
await this.props.hideApiKeys();
|
||||||
@@ -165,7 +118,6 @@ export class ApiKeysPageUnconnected extends PureComponent<Props, State> {
|
|||||||
includeExpired,
|
includeExpired,
|
||||||
includeExpiredDisabled,
|
includeExpiredDisabled,
|
||||||
canCreate,
|
canCreate,
|
||||||
apiKeysMigrated,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!hasFetched) {
|
if (!hasFetched) {
|
||||||
@@ -180,37 +132,21 @@ export class ApiKeysPageUnconnected extends PureComponent<Props, State> {
|
|||||||
<Page {...defaultPageProps}>
|
<Page {...defaultPageProps}>
|
||||||
<Page.Contents isLoading={false}>
|
<Page.Contents isLoading={false}>
|
||||||
<ApiKeysController>
|
<ApiKeysController>
|
||||||
{({ isAdding, toggleIsAdding }) => {
|
{({}) => {
|
||||||
const showCTA = !isAdding && apiKeysCount === 0 && !apiKeysMigrated;
|
|
||||||
const showTable = apiKeysCount > 0;
|
const showTable = apiKeysCount > 0;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!apiKeysMigrated && <MigrateToServiceAccountsCard onMigrate={this.onMigrateAll} />}
|
{apiKeysCount !== 0 && <MigrateToServiceAccountsCard onMigrate={this.onMigrateAll} />}
|
||||||
{apiKeysMigrated && <APIKeysMigratedCard onHideApiKeys={this.onHideApiKeys} />}
|
{apiKeysCount === 0 && (
|
||||||
{showCTA ? (
|
<APIKeysMigratedCard onHideApiKeys={this.onHideApiKeys} apikeys={apiKeysCount} />
|
||||||
<EmptyListCTA
|
)}
|
||||||
title="You haven't added any API keys yet."
|
|
||||||
buttonIcon="key-skeleton-alt"
|
|
||||||
onClick={toggleIsAdding}
|
|
||||||
buttonTitle="New API key"
|
|
||||||
proTip="Remember, you can provide view-only API access to other applications."
|
|
||||||
buttonDisabled={!canCreate}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{showTable ? (
|
{showTable ? (
|
||||||
<ApiKeysActionBar
|
<ApiKeysActionBar
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
disabled={isAdding || !canCreate}
|
disabled={!canCreate}
|
||||||
onAddClick={toggleIsAdding}
|
|
||||||
onSearchChange={this.onSearchQueryChange}
|
onSearchChange={this.onSearchQueryChange}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<ApiKeysForm
|
|
||||||
show={isAdding}
|
|
||||||
onClose={toggleIsAdding}
|
|
||||||
onKeyAdded={this.onAddApiKey}
|
|
||||||
disabled={!canCreate}
|
|
||||||
/>
|
|
||||||
{showTable ? (
|
{showTable ? (
|
||||||
<VerticalGroup>
|
<VerticalGroup>
|
||||||
<InlineField disabled={includeExpiredDisabled} label="Include expired keys">
|
<InlineField disabled={includeExpiredDisabled} label="Include expired keys">
|
||||||
|
|||||||
@@ -16,26 +16,24 @@ export const MigrateToServiceAccountsCard = ({ onMigrate, disabled }: Props): JS
|
|||||||
const docsLink = (
|
const docsLink = (
|
||||||
<a
|
<a
|
||||||
className="external-link"
|
className="external-link"
|
||||||
href="https://grafana.com/docs/grafana/latest/administration/api-keys/#migrate-api-keys-to-grafana-service-accounts/"
|
href="https://grafana.com/docs/grafana/latest/administration/api-keys/#migrate-api-keys-to-grafana-service-accounts"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
here.
|
Find out more about the migration here.
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
const migrationBoxDesc = (
|
const migrationBoxDesc = <span>Are you sure you want to migrate all API keys to service accounts? {docsLink}</span>;
|
||||||
<span>Are you sure you want to migrate all API keys to service accounts? Find out more {docsLink}</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert title="Switch from API keys to service accounts" severity="info">
|
<Alert title="Switch from API keys to service accounts" severity="warning">
|
||||||
<div className={styles.text}>
|
<div className={styles.text}>
|
||||||
Each API key will be automatically migrated into a service account with a token. The service account will be
|
We will soon deprecate API keys. Each API key will be migrated into a service account with a token and will
|
||||||
created with the same permission as the API Key and current API Keys will continue to work as they were.
|
continue to work as they were. We encourage you to migrate your API keys to service accounts now. {docsLink}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.actionRow}>
|
<div className={styles.actionRow}>
|
||||||
<Button className={styles.actionButton} onClick={() => setIsModalOpen(true)}>
|
<Button className={styles.actionButton} onClick={() => setIsModalOpen(true)}>
|
||||||
Migrate to service accounts now
|
Migrate all service accounts
|
||||||
</Button>
|
</Button>
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
title={'Migrate API keys to service accounts'}
|
title={'Migrate API keys to service accounts'}
|
||||||
@@ -44,6 +42,8 @@ export const MigrateToServiceAccountsCard = ({ onMigrate, disabled }: Props): JS
|
|||||||
confirmText={'Yes, migrate now'}
|
confirmText={'Yes, migrate now'}
|
||||||
onConfirm={onMigrate}
|
onConfirm={onMigrate}
|
||||||
onDismiss={() => setIsModalOpen(false)}
|
onDismiss={() => setIsModalOpen(false)}
|
||||||
|
confirmVariant="primary"
|
||||||
|
confirmButtonVariant="primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|||||||
@@ -1,24 +1,7 @@
|
|||||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
import store from 'app/core/store';
|
import { ThunkResult } from 'app/types';
|
||||||
import { API_KEYS_MIGRATION_INFO_STORAGE_KEY } from 'app/features/serviceaccounts/constants';
|
|
||||||
import { ApiKey, ThunkResult } from 'app/types';
|
|
||||||
|
|
||||||
import {
|
import { apiKeysLoaded, includeExpiredToggled, isFetching } from './reducers';
|
||||||
apiKeysLoaded,
|
|
||||||
includeExpiredToggled,
|
|
||||||
isFetching,
|
|
||||||
apiKeysMigrationStatusLoaded,
|
|
||||||
setSearchQuery,
|
|
||||||
} from './reducers';
|
|
||||||
|
|
||||||
export function addApiKey(apiKey: ApiKey, openModal: (key: string) => void): ThunkResult<void> {
|
|
||||||
return async (dispatch) => {
|
|
||||||
const result = await getBackendSrv().post('/api/auth/keys', apiKey);
|
|
||||||
dispatch(setSearchQuery(''));
|
|
||||||
dispatch(loadApiKeys());
|
|
||||||
openModal(result.key);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadApiKeys(): ThunkResult<void> {
|
export function loadApiKeys(): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
@@ -53,21 +36,12 @@ export function migrateAll(): ThunkResult<void> {
|
|||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
try {
|
try {
|
||||||
await getBackendSrv().post('/api/serviceaccounts/migrate');
|
await getBackendSrv().post('/api/serviceaccounts/migrate');
|
||||||
store.set(API_KEYS_MIGRATION_INFO_STORAGE_KEY, true);
|
|
||||||
} finally {
|
} finally {
|
||||||
dispatch(getApiKeysMigrationStatus());
|
|
||||||
dispatch(loadApiKeys());
|
dispatch(loadApiKeys());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getApiKeysMigrationStatus(): ThunkResult<void> {
|
|
||||||
return async (dispatch) => {
|
|
||||||
const result = await getBackendSrv().get('/api/serviceaccounts/migrationstatus');
|
|
||||||
dispatch(apiKeysMigrationStatusLoaded(!!result?.migrated));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hideApiKeys(): ThunkResult<void> {
|
export function hideApiKeys(): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
await getBackendSrv().post('/api/serviceaccounts/hideApiKeys');
|
await getBackendSrv().post('/api/serviceaccounts/hideApiKeys');
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export const initialApiKeysState: ApiKeysState = {
|
|||||||
keys: [],
|
keys: [],
|
||||||
keysIncludingExpired: [],
|
keysIncludingExpired: [],
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
apiKeysMigrated: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiKeysSlice = createSlice({
|
const apiKeysSlice = createSlice({
|
||||||
@@ -23,9 +22,6 @@ const apiKeysSlice = createSlice({
|
|||||||
: state.includeExpired;
|
: state.includeExpired;
|
||||||
return { ...state, hasFetched: true, keys, keysIncludingExpired, includeExpired };
|
return { ...state, hasFetched: true, keys, keysIncludingExpired, includeExpired };
|
||||||
},
|
},
|
||||||
apiKeysMigrationStatusLoaded: (state, action): ApiKeysState => {
|
|
||||||
return { ...state, apiKeysMigrated: action.payload };
|
|
||||||
},
|
|
||||||
setSearchQuery: (state, action): ApiKeysState => {
|
setSearchQuery: (state, action): ApiKeysState => {
|
||||||
return { ...state, searchQuery: action.payload };
|
return { ...state, searchQuery: action.payload };
|
||||||
},
|
},
|
||||||
@@ -38,8 +34,7 @@ const apiKeysSlice = createSlice({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { apiKeysLoaded, includeExpiredToggled, isFetching, setSearchQuery, apiKeysMigrationStatusLoaded } =
|
export const { apiKeysLoaded, includeExpiredToggled, isFetching, setSearchQuery } = apiKeysSlice.actions;
|
||||||
apiKeysSlice.actions;
|
|
||||||
|
|
||||||
export const apiKeysReducer = apiKeysSlice.reducer;
|
export const apiKeysReducer = apiKeysSlice.reducer;
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ describe('API Keys selectors', () => {
|
|||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
hasFetched: true,
|
hasFetched: true,
|
||||||
includeExpired: false,
|
includeExpired: false,
|
||||||
apiKeysMigrated: false,
|
|
||||||
};
|
};
|
||||||
const keyCount = getApiKeysCount(mockState);
|
const keyCount = getApiKeysCount(mockState);
|
||||||
expect(keyCount).toBe(5);
|
expect(keyCount).toBe(5);
|
||||||
@@ -29,7 +28,6 @@ describe('API Keys selectors', () => {
|
|||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
hasFetched: true,
|
hasFetched: true,
|
||||||
includeExpired: true,
|
includeExpired: true,
|
||||||
apiKeysMigrated: false,
|
|
||||||
};
|
};
|
||||||
const keyCount = getApiKeysCount(mockState);
|
const keyCount = getApiKeysCount(mockState);
|
||||||
expect(keyCount).toBe(8);
|
expect(keyCount).toBe(8);
|
||||||
@@ -45,7 +43,6 @@ describe('API Keys selectors', () => {
|
|||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
hasFetched: true,
|
hasFetched: true,
|
||||||
includeExpired: false,
|
includeExpired: false,
|
||||||
apiKeysMigrated: false,
|
|
||||||
};
|
};
|
||||||
const keys = getApiKeys(mockState);
|
const keys = getApiKeys(mockState);
|
||||||
expect(keys).toEqual(mockKeys);
|
expect(keys).toEqual(mockKeys);
|
||||||
@@ -58,7 +55,6 @@ describe('API Keys selectors', () => {
|
|||||||
searchQuery: '5',
|
searchQuery: '5',
|
||||||
hasFetched: true,
|
hasFetched: true,
|
||||||
includeExpired: false,
|
includeExpired: false,
|
||||||
apiKeysMigrated: false,
|
|
||||||
};
|
};
|
||||||
const keys = getApiKeys(mockState);
|
const keys = getApiKeys(mockState);
|
||||||
expect(keys.length).toEqual(1);
|
expect(keys.length).toEqual(1);
|
||||||
@@ -73,7 +69,6 @@ describe('API Keys selectors', () => {
|
|||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
hasFetched: true,
|
hasFetched: true,
|
||||||
includeExpired: true,
|
includeExpired: true,
|
||||||
apiKeysMigrated: false,
|
|
||||||
};
|
};
|
||||||
const keys = getApiKeys(mockState);
|
const keys = getApiKeys(mockState);
|
||||||
expect(keys).toEqual(mockKeysIncludingExpired);
|
expect(keys).toEqual(mockKeysIncludingExpired);
|
||||||
@@ -86,7 +81,6 @@ describe('API Keys selectors', () => {
|
|||||||
searchQuery: '5',
|
searchQuery: '5',
|
||||||
hasFetched: true,
|
hasFetched: true,
|
||||||
includeExpired: true,
|
includeExpired: true,
|
||||||
apiKeysMigrated: false,
|
|
||||||
};
|
};
|
||||||
const keys = getApiKeys(mockState);
|
const keys = getApiKeys(mockState);
|
||||||
expect(keys.length).toEqual(1);
|
expect(keys.length).toEqual(1);
|
||||||
@@ -102,7 +96,6 @@ describe('API Keys selectors', () => {
|
|||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
hasFetched: true,
|
hasFetched: true,
|
||||||
includeExpired: true,
|
includeExpired: true,
|
||||||
apiKeysMigrated: false,
|
|
||||||
};
|
};
|
||||||
const includeExpired = getIncludeExpired(mockState);
|
const includeExpired = getIncludeExpired(mockState);
|
||||||
expect(includeExpired).toBe(true);
|
expect(includeExpired).toBe(true);
|
||||||
@@ -115,7 +108,6 @@ describe('API Keys selectors', () => {
|
|||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
hasFetched: true,
|
hasFetched: true,
|
||||||
includeExpired: false,
|
includeExpired: false,
|
||||||
apiKeysMigrated: false,
|
|
||||||
};
|
};
|
||||||
const includeExpired = getIncludeExpired(mockState);
|
const includeExpired = getIncludeExpired(mockState);
|
||||||
expect(includeExpired).toBe(false);
|
expect(includeExpired).toBe(false);
|
||||||
@@ -130,7 +122,6 @@ describe('API Keys selectors', () => {
|
|||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
hasFetched: true,
|
hasFetched: true,
|
||||||
includeExpired: true,
|
includeExpired: true,
|
||||||
apiKeysMigrated: false,
|
|
||||||
};
|
};
|
||||||
const includeExpiredDisabled = getIncludeExpiredDisabled(mockState);
|
const includeExpiredDisabled = getIncludeExpiredDisabled(mockState);
|
||||||
expect(includeExpiredDisabled).toBe(true);
|
expect(includeExpiredDisabled).toBe(true);
|
||||||
@@ -143,7 +134,6 @@ describe('API Keys selectors', () => {
|
|||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
hasFetched: true,
|
hasFetched: true,
|
||||||
includeExpired: false,
|
includeExpired: false,
|
||||||
apiKeysMigrated: false,
|
|
||||||
};
|
};
|
||||||
const includeExpiredDisabled = getIncludeExpired(mockState);
|
const includeExpiredDisabled = getIncludeExpired(mockState);
|
||||||
expect(includeExpiredDisabled).toBe(false);
|
expect(includeExpiredDisabled).toBe(false);
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ const setup = (propOverrides: Partial<Props>) => {
|
|||||||
const updateServiceAccountMock = jest.fn();
|
const updateServiceAccountMock = jest.fn();
|
||||||
const changeStateFilterMock = jest.fn();
|
const changeStateFilterMock = jest.fn();
|
||||||
const createServiceAccountTokenMock = jest.fn();
|
const createServiceAccountTokenMock = jest.fn();
|
||||||
const getApiKeysMigrationStatusMock = jest.fn();
|
|
||||||
const getApiKeysMigrationInfoMock = jest.fn();
|
|
||||||
const closeApiKeysMigrationInfoMock = jest.fn();
|
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
page: 0,
|
page: 0,
|
||||||
@@ -36,8 +33,6 @@ const setup = (propOverrides: Partial<Props>) => {
|
|||||||
showPaging: false,
|
showPaging: false,
|
||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
serviceAccounts: [],
|
serviceAccounts: [],
|
||||||
apiKeysMigrated: false,
|
|
||||||
showApiKeysMigrationInfo: false,
|
|
||||||
changeQuery: changeQueryMock,
|
changeQuery: changeQueryMock,
|
||||||
fetchACOptions: fetchACOptionsMock,
|
fetchACOptions: fetchACOptionsMock,
|
||||||
fetchServiceAccounts: fetchServiceAccountsMock,
|
fetchServiceAccounts: fetchServiceAccountsMock,
|
||||||
@@ -45,9 +40,6 @@ const setup = (propOverrides: Partial<Props>) => {
|
|||||||
updateServiceAccount: updateServiceAccountMock,
|
updateServiceAccount: updateServiceAccountMock,
|
||||||
changeStateFilter: changeStateFilterMock,
|
changeStateFilter: changeStateFilterMock,
|
||||||
createServiceAccountToken: createServiceAccountTokenMock,
|
createServiceAccountToken: createServiceAccountTokenMock,
|
||||||
getApiKeysMigrationStatus: getApiKeysMigrationStatusMock,
|
|
||||||
getApiKeysMigrationInfo: getApiKeysMigrationInfoMock,
|
|
||||||
closeApiKeysMigrationInfo: closeApiKeysMigrationInfoMock,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
Object.assign(props, propOverrides);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { connect, ConnectedProps } from 'react-redux';
|
import { connect, ConnectedProps } from 'react-redux';
|
||||||
|
|
||||||
import { GrafanaTheme2, OrgRole } from '@grafana/data';
|
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 EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||||
@@ -21,9 +21,6 @@ import {
|
|||||||
updateServiceAccount,
|
updateServiceAccount,
|
||||||
changeStateFilter,
|
changeStateFilter,
|
||||||
createServiceAccountToken,
|
createServiceAccountToken,
|
||||||
getApiKeysMigrationStatus,
|
|
||||||
getApiKeysMigrationInfo,
|
|
||||||
closeApiKeysMigrationInfo,
|
|
||||||
} from './state/actions';
|
} from './state/actions';
|
||||||
|
|
||||||
interface OwnProps {}
|
interface OwnProps {}
|
||||||
@@ -44,9 +41,6 @@ const mapDispatchToProps = {
|
|||||||
updateServiceAccount,
|
updateServiceAccount,
|
||||||
changeStateFilter,
|
changeStateFilter,
|
||||||
createServiceAccountToken,
|
createServiceAccountToken,
|
||||||
getApiKeysMigrationStatus,
|
|
||||||
getApiKeysMigrationInfo,
|
|
||||||
closeApiKeysMigrationInfo,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||||
@@ -57,8 +51,6 @@ export const ServiceAccountsListPageUnconnected = ({
|
|||||||
roleOptions,
|
roleOptions,
|
||||||
query,
|
query,
|
||||||
serviceAccountStateFilter,
|
serviceAccountStateFilter,
|
||||||
apiKeysMigrated,
|
|
||||||
showApiKeysMigrationInfo,
|
|
||||||
changeQuery,
|
changeQuery,
|
||||||
fetchACOptions,
|
fetchACOptions,
|
||||||
fetchServiceAccounts,
|
fetchServiceAccounts,
|
||||||
@@ -66,9 +58,6 @@ export const ServiceAccountsListPageUnconnected = ({
|
|||||||
updateServiceAccount,
|
updateServiceAccount,
|
||||||
changeStateFilter,
|
changeStateFilter,
|
||||||
createServiceAccountToken,
|
createServiceAccountToken,
|
||||||
getApiKeysMigrationStatus,
|
|
||||||
getApiKeysMigrationInfo,
|
|
||||||
closeApiKeysMigrationInfo,
|
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
@@ -79,12 +68,10 @@ export const ServiceAccountsListPageUnconnected = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchServiceAccounts({ withLoadingIndicator: true });
|
fetchServiceAccounts({ withLoadingIndicator: true });
|
||||||
getApiKeysMigrationStatus();
|
|
||||||
getApiKeysMigrationInfo();
|
|
||||||
if (contextSrv.licensedAccessControlEnabled()) {
|
if (contextSrv.licensedAccessControlEnabled()) {
|
||||||
fetchACOptions();
|
fetchACOptions();
|
||||||
}
|
}
|
||||||
}, [fetchACOptions, fetchServiceAccounts, getApiKeysMigrationStatus, getApiKeysMigrationInfo]);
|
}, [fetchACOptions, fetchServiceAccounts]);
|
||||||
|
|
||||||
const noServiceAccountsCreated =
|
const noServiceAccountsCreated =
|
||||||
serviceAccounts.length === 0 && serviceAccountStateFilter === ServiceAccountStateFilter.All && !query;
|
serviceAccounts.length === 0 && serviceAccountStateFilter === ServiceAccountStateFilter.All && !query;
|
||||||
@@ -160,10 +147,6 @@ export const ServiceAccountsListPageUnconnected = ({
|
|||||||
setCurrentServiceAccount(null);
|
setCurrentServiceAccount(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMigrationInfoClose = () => {
|
|
||||||
closeApiKeysMigrationInfo();
|
|
||||||
};
|
|
||||||
|
|
||||||
const docsLink = (
|
const docsLink = (
|
||||||
<a
|
<a
|
||||||
className="external-link"
|
className="external-link"
|
||||||
@@ -183,14 +166,6 @@ export const ServiceAccountsListPageUnconnected = ({
|
|||||||
return (
|
return (
|
||||||
<Page navId="serviceaccounts" subTitle={subTitle}>
|
<Page navId="serviceaccounts" subTitle={subTitle}>
|
||||||
<Page.Contents>
|
<Page.Contents>
|
||||||
{apiKeysMigrated && showApiKeysMigrationInfo && (
|
|
||||||
<Alert
|
|
||||||
title="API keys migrated to Service accounts. Your keys are now called tokens and live inside respective service
|
|
||||||
accounts. Learn more."
|
|
||||||
severity="success"
|
|
||||||
onRemove={onMigrationInfoClose}
|
|
||||||
></Alert>
|
|
||||||
)}
|
|
||||||
<Page.OldNavOnly>
|
<Page.OldNavOnly>
|
||||||
<div className={styles.pageHeader}>
|
<div className={styles.pageHeader}>
|
||||||
<h2>Service accounts</h2>
|
<h2>Service accounts</h2>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export const API_KEYS_MIGRATION_INFO_STORAGE_KEY = 'grafana.serviceaccounts.showApiKeysMigrationInfo';
|
|
||||||
@@ -3,11 +3,9 @@ import { debounce } from 'lodash';
|
|||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { fetchRoleOptions } from 'app/core/components/RolePicker/api';
|
import { fetchRoleOptions } from 'app/core/components/RolePicker/api';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import store from 'app/core/store';
|
|
||||||
import { AccessControlAction, ServiceAccountDTO, ServiceAccountStateFilter, ThunkResult } from 'app/types';
|
import { AccessControlAction, ServiceAccountDTO, ServiceAccountStateFilter, ThunkResult } from 'app/types';
|
||||||
|
|
||||||
import { ServiceAccountToken } from '../components/CreateTokenModal';
|
import { ServiceAccountToken } from '../components/CreateTokenModal';
|
||||||
import { API_KEYS_MIGRATION_INFO_STORAGE_KEY } from '../constants';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
acOptionsLoaded,
|
acOptionsLoaded,
|
||||||
@@ -16,9 +14,7 @@ import {
|
|||||||
serviceAccountsFetchBegin,
|
serviceAccountsFetchBegin,
|
||||||
serviceAccountsFetched,
|
serviceAccountsFetched,
|
||||||
serviceAccountsFetchEnd,
|
serviceAccountsFetchEnd,
|
||||||
apiKeysMigrationStatusLoaded,
|
|
||||||
stateFilterChanged,
|
stateFilterChanged,
|
||||||
showApiKeysMigrationInfoLoaded,
|
|
||||||
} from './reducers';
|
} from './reducers';
|
||||||
|
|
||||||
const BASE_URL = `/api/serviceaccounts`;
|
const BASE_URL = `/api/serviceaccounts`;
|
||||||
@@ -36,15 +32,6 @@ export function fetchACOptions(): ThunkResult<void> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getApiKeysMigrationStatus(): ThunkResult<void> {
|
|
||||||
return async (dispatch) => {
|
|
||||||
if (contextSrv.hasPermission(AccessControlAction.ServiceAccountsRead)) {
|
|
||||||
const result = await getBackendSrv().get('/api/serviceaccounts/migrationstatus');
|
|
||||||
dispatch(apiKeysMigrationStatusLoaded(!!result?.migrated));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FetchServiceAccountsParams {
|
interface FetchServiceAccountsParams {
|
||||||
withLoadingIndicator: boolean;
|
withLoadingIndicator: boolean;
|
||||||
}
|
}
|
||||||
@@ -138,17 +125,3 @@ export function changePage(page: number): ThunkResult<void> {
|
|||||||
dispatch(fetchServiceAccounts());
|
dispatch(fetchServiceAccounts());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getApiKeysMigrationInfo(): ThunkResult<void> {
|
|
||||||
return async (dispatch) => {
|
|
||||||
const showApiKeysMigrationInfo = store.getBool(API_KEYS_MIGRATION_INFO_STORAGE_KEY, false);
|
|
||||||
dispatch(showApiKeysMigrationInfoLoaded(showApiKeysMigrationInfo));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function closeApiKeysMigrationInfo(): ThunkResult<void> {
|
|
||||||
return async (dispatch) => {
|
|
||||||
store.set(API_KEYS_MIGRATION_INFO_STORAGE_KEY, false);
|
|
||||||
dispatch(getApiKeysMigrationInfo());
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -50,8 +50,6 @@ export const initialStateList: ServiceAccountsState = {
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
showPaging: false,
|
showPaging: false,
|
||||||
serviceAccountStateFilter: ServiceAccountStateFilter.All,
|
serviceAccountStateFilter: ServiceAccountStateFilter.All,
|
||||||
apiKeysMigrated: false,
|
|
||||||
showApiKeysMigrationInfo: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ServiceAccountsFetched {
|
interface ServiceAccountsFetched {
|
||||||
@@ -87,12 +85,6 @@ const serviceAccountsSlice = createSlice({
|
|||||||
acOptionsLoaded: (state, action: PayloadAction<Role[]>): ServiceAccountsState => {
|
acOptionsLoaded: (state, action: PayloadAction<Role[]>): ServiceAccountsState => {
|
||||||
return { ...state, roleOptions: action.payload };
|
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<string>) => {
|
queryChanged: (state, action: PayloadAction<string>) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -117,8 +109,6 @@ export const {
|
|||||||
serviceAccountsFetchEnd,
|
serviceAccountsFetchEnd,
|
||||||
serviceAccountsFetched,
|
serviceAccountsFetched,
|
||||||
acOptionsLoaded,
|
acOptionsLoaded,
|
||||||
apiKeysMigrationStatusLoaded,
|
|
||||||
showApiKeysMigrationInfoLoaded,
|
|
||||||
pageChanged,
|
pageChanged,
|
||||||
stateFilterChanged,
|
stateFilterChanged,
|
||||||
queryChanged,
|
queryChanged,
|
||||||
|
|||||||
@@ -15,17 +15,10 @@ export interface ApiKey extends WithAccessControlMetadata {
|
|||||||
lastUsedAt?: string;
|
lastUsedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NewApiKey {
|
|
||||||
name: string;
|
|
||||||
role: OrgRole;
|
|
||||||
secondsToLive: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApiKeysState {
|
export interface ApiKeysState {
|
||||||
includeExpired: boolean;
|
includeExpired: boolean;
|
||||||
keys: ApiKey[];
|
keys: ApiKey[];
|
||||||
keysIncludingExpired: ApiKey[];
|
keysIncludingExpired: ApiKey[];
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
hasFetched: boolean;
|
hasFetched: boolean;
|
||||||
apiKeysMigrated: boolean;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,8 +65,6 @@ export interface ServiceAccountsState {
|
|||||||
serviceAccounts: ServiceAccountDTO[];
|
serviceAccounts: ServiceAccountDTO[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
roleOptions: Role[];
|
roleOptions: Role[];
|
||||||
apiKeysMigrated: boolean;
|
|
||||||
showApiKeysMigrationInfo: boolean;
|
|
||||||
|
|
||||||
// search / filtering
|
// search / filtering
|
||||||
query: string;
|
query: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user