From bc5237e8409ad1385cc6db0bc3f9c170b5db127d Mon Sep 17 00:00:00 2001 From: J Guerreiro Date: Tue, 1 Mar 2022 08:21:55 +0000 Subject: [PATCH] Service Accounts: Link final components in service accounts detail page (#45929) * ServiceAccounts: Delete/Disable service account from details page * ServiceAccounts: capitalize viewable messages from UI * ServiceAccounts: Link new update endpoint to details page * ServiceAccounts: reimplement service account retrieve to include is_disabled and only target service accounts * Cleanup styles * Fix modal show * ServiceAccounts: simplify handler functions * Apply suggestions from code review Co-authored-by: Alex Khomenko Co-authored-by: Clarity-89 Co-authored-by: Alex Khomenko --- pkg/services/serviceaccounts/api/api.go | 14 +-- pkg/services/serviceaccounts/api/token.go | 6 +- .../serviceaccounts/database/database.go | 104 +++++++++++------- pkg/services/serviceaccounts/models.go | 27 ++--- .../serviceaccounts/serviceaccounts.go | 2 +- pkg/services/serviceaccounts/tests/common.go | 2 +- .../serviceaccounts/ServiceAccountPage.tsx | 28 ++--- .../serviceaccounts/ServiceAccountProfile.tsx | 84 +++++++------- .../serviceaccounts/ServiceAccountRoleRow.tsx | 6 +- .../features/serviceaccounts/state/actions.ts | 13 ++- 10 files changed, 160 insertions(+), 126 deletions(-) diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index bce3a4d8a6c..628597878ff 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -109,12 +109,12 @@ func (api *ServiceAccountsAPI) DeleteServiceAccount(ctx *models.ReqContext) resp if err != nil { return response.Error(http.StatusInternalServerError, "Service account deletion error", err) } - return response.Success("service account deleted") + return response.Success("Service account deleted") } func (api *ServiceAccountsAPI) UpgradeServiceAccounts(ctx *models.ReqContext) response.Response { if err := api.store.UpgradeServiceAccounts(ctx.Req.Context()); err == nil { - return response.Success("service accounts upgraded") + return response.Success("Service accounts upgraded") } else { return response.Error(http.StatusInternalServerError, "Internal server error", err) } @@ -123,10 +123,10 @@ func (api *ServiceAccountsAPI) UpgradeServiceAccounts(ctx *models.ReqContext) re func (api *ServiceAccountsAPI) ConvertToServiceAccount(ctx *models.ReqContext) response.Response { keyId, err := strconv.ParseInt(web.Params(ctx.Req)[":keyId"], 10, 64) if err != nil { - return response.Error(http.StatusBadRequest, "keyId is invalid", err) + return response.Error(http.StatusBadRequest, "Key ID is invalid", err) } if err := api.store.ConvertToServiceAccounts(ctx.Req.Context(), []int64{keyId}); err == nil { - return response.Success("service accounts converted") + return response.Success("Service accounts converted") } else { return response.Error(500, "Internal server error", err) } @@ -174,7 +174,7 @@ func (api *ServiceAccountsAPI) getAccessControlMetadata(c *models.ReqContext, sa func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *models.ReqContext) response.Response { scopeID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64) if err != nil { - return response.Error(http.StatusBadRequest, "serviceAccountId is invalid", err) + return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) } serviceAccount, err := api.store.RetrieveServiceAccount(ctx.Req.Context(), ctx.OrgId, scopeID) @@ -197,12 +197,12 @@ func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *models.ReqContext) re func (api *ServiceAccountsAPI) updateServiceAccount(c *models.ReqContext) response.Response { scopeID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64) if err != nil { - return response.Error(http.StatusBadRequest, "serviceAccountId is invalid", err) + return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) } cmd := &serviceaccounts.UpdateServiceAccountForm{} if err := web.Bind(c.Req, &cmd); err != nil { - return response.Error(http.StatusBadRequest, "bad request data", err) + return response.Error(http.StatusBadRequest, "Bad request data", err) } if cmd.Role != nil && !cmd.Role.IsValid() { diff --git a/pkg/services/serviceaccounts/api/token.go b/pkg/services/serviceaccounts/api/token.go index 7347084501a..787d06fdadb 100644 --- a/pkg/services/serviceaccounts/api/token.go +++ b/pkg/services/serviceaccounts/api/token.go @@ -39,7 +39,7 @@ const sevenDaysAhead = 7 * 24 * time.Hour func (api *ServiceAccountsAPI) ListTokens(ctx *models.ReqContext) response.Response { saID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64) if err != nil { - return response.Error(http.StatusBadRequest, "serviceAccountId is invalid", err) + return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) } if saTokens, err := api.store.ListTokens(ctx.Req.Context(), ctx.OrgId, saID); err == nil { @@ -78,7 +78,7 @@ func (api *ServiceAccountsAPI) ListTokens(ctx *models.ReqContext) response.Respo func (api *ServiceAccountsAPI) CreateToken(c *models.ReqContext) response.Response { saID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64) if err != nil { - return response.Error(http.StatusBadRequest, "serviceAccountId is invalid", err) + return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) } // confirm service account exists @@ -93,7 +93,7 @@ func (api *ServiceAccountsAPI) CreateToken(c *models.ReqContext) response.Respon cmd := models.AddApiKeyCommand{} if err := web.Bind(c.Req, &cmd); err != nil { - return response.Error(http.StatusBadRequest, "bad request data", err) + return response.Error(http.StatusBadRequest, "Bad request data", err) } // Force affected service account to be the one referenced in the URL diff --git a/pkg/services/serviceaccounts/database/database.go b/pkg/services/serviceaccounts/database/database.go index d9ec41dd7e1..c83abb94db9 100644 --- a/pkg/services/serviceaccounts/database/database.go +++ b/pkg/services/serviceaccounts/database/database.go @@ -4,6 +4,7 @@ package database import ( "context" "fmt" + "strings" "time" "github.com/google/uuid" @@ -169,14 +170,53 @@ func (s *ServiceAccountsStoreImpl) ListServiceAccounts(ctx context.Context, orgI // RetrieveServiceAccountByID returns a service account by its ID func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { - query := models.GetOrgUsersQuery{UserID: serviceAccountID, OrgId: orgID, IsServiceAccount: true} - err := s.sqlStore.GetOrgUsers(ctx, &query) + serviceAccount := &serviceaccounts.ServiceAccountProfileDTO{} + + err := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + sess := dbSession.Table("org_user") + sess.Join("INNER", s.sqlStore.Dialect.Quote("user"), + fmt.Sprintf("org_user.user_id=%s.id", s.sqlStore.Dialect.Quote("user"))) + + whereConditions := make([]string, 0, 3) + whereParams := make([]interface{}, 0) + + whereConditions = append(whereConditions, "org_user.org_id = ?") + whereParams = append(whereParams, orgID) + + whereConditions = append(whereConditions, "org_user.user_id = ?") + whereParams = append(whereParams, serviceAccountID) + + whereConditions = append(whereConditions, + fmt.Sprintf("%s.is_service_account = %s", + s.sqlStore.Dialect.Quote("user"), + s.sqlStore.Dialect.BooleanStr(true))) + + sess.Where(strings.Join(whereConditions, " AND "), whereParams...) + + sess.Cols( + "org_user.user_id", + "org_user.org_id", + "org_user.role", + "user.email", + "user.name", + "user.login", + "user.created", + "user.updated", + "user.is_disabled", + ) + + if ok, err := sess.Get(serviceAccount); err != nil { + return err + } else if !ok { + return serviceaccounts.ErrServiceAccountNotFound + } + + return nil + }) + if err != nil { return nil, err } - if len(query.Result) != 1 { - return nil, serviceaccounts.ErrServiceAccountNotFound - } // Get Teams of service account. Can be optimized by combining with the query above // in refactor @@ -190,36 +230,24 @@ func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, o teams[i] = getTeamQuery.Result[i].Name } - saProfile := &serviceaccounts.ServiceAccountProfileDTO{ - Id: query.Result[0].UserId, - Name: query.Result[0].Name, - Login: query.Result[0].Login, - OrgId: query.Result[0].OrgId, - UpdatedAt: query.Result[0].Updated, - CreatedAt: query.Result[0].Created, - Role: query.Result[0].Role, - Teams: teams, - } - return saProfile, nil + serviceAccount.Teams = teams + + return serviceAccount, nil } func (s *ServiceAccountsStoreImpl) UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, - saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { - updatedUser := &models.OrgUserDTO{} + saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) { + updatedUser := &serviceaccounts.ServiceAccountProfileDTO{} err := s.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { - query := models.GetOrgUsersQuery{UserID: serviceAccountID, OrgId: orgID, IsServiceAccount: true} - if err := s.sqlStore.GetOrgUsers(ctx, &query); err != nil { + var err error + updatedUser, err = s.RetrieveServiceAccount(ctx, orgID, serviceAccountID) + if err != nil { return err } - if len(query.Result) != 1 { - return serviceaccounts.ErrServiceAccountNotFound - } - updatedUser = query.Result[0] - - if saForm.Name == nil && saForm.Role == nil { + if saForm.Name == nil && saForm.Role == nil && saForm.IsDisabled == nil { return nil } @@ -236,29 +264,31 @@ func (s *ServiceAccountsStoreImpl) UpdateServiceAccount(ctx context.Context, updatedUser.Role = string(*saForm.Role) } - if saForm.Name != nil { + if saForm.Name != nil || saForm.IsDisabled != nil { user := models.User{ - Name: *saForm.Name, Updated: updateTime, } + if saForm.IsDisabled != nil { + user.IsDisabled = *saForm.IsDisabled + updatedUser.IsDisabled = *saForm.IsDisabled + sess.UseBool("is_disabled") + } + + if saForm.Name != nil { + user.Name = *saForm.Name + updatedUser.Name = *saForm.Name + } + if _, err := sess.ID(serviceAccountID).Update(&user); err != nil { return err } - - updatedUser.Name = *saForm.Name } return nil }) - return &serviceaccounts.ServiceAccountDTO{ - Id: updatedUser.UserId, - Name: updatedUser.Name, - Login: updatedUser.Login, - Role: updatedUser.Role, - OrgId: updatedUser.OrgId, - }, err + return updatedUser, err } func contains(s []int64, e int64) bool { diff --git a/pkg/services/serviceaccounts/models.go b/pkg/services/serviceaccounts/models.go index bd0d9e54108..7bd167dad25 100644 --- a/pkg/services/serviceaccounts/models.go +++ b/pkg/services/serviceaccounts/models.go @@ -24,8 +24,9 @@ type ServiceAccount struct { } type UpdateServiceAccountForm struct { - Name *string `json:"name"` - Role *models.RoleType `json:"role"` + Name *string `json:"name"` + Role *models.RoleType `json:"role"` + IsDisabled *bool `json:"isDisabled"` } type CreateServiceAccountForm struct { @@ -45,15 +46,15 @@ type ServiceAccountDTO struct { } type ServiceAccountProfileDTO struct { - Id int64 `json:"id"` - Name string `json:"name"` - Login string `json:"login"` - OrgId int64 `json:"orgId"` - IsDisabled bool `json:"isDisabled"` - UpdatedAt time.Time `json:"updatedAt"` - CreatedAt time.Time `json:"createdAt"` - AvatarUrl string `json:"avatarUrl"` - Role string `json:"role"` - Teams []string `json:"teams"` - AccessControl map[string]bool `json:"accessControl,omitempty"` + Id int64 `json:"id" xorm:"user_id"` + Name string `json:"name" xorm:"name"` + Login string `json:"login" xorm:"login"` + OrgId int64 `json:"orgId" xorm:"org_id"` + IsDisabled bool `json:"isDisabled" xorm:"is_disabled"` + Created time.Time `json:"createdAt" xorm:"created"` + Updated time.Time `json:"updatedAt" xorm:"updated"` + AvatarUrl string `json:"avatarUrl" xorm:"-"` + Role string `json:"role" xorm:"role"` + Teams []string `json:"teams" xorm:"-"` + AccessControl map[string]bool `json:"accessControl,omitempty" xorm:"-"` } diff --git a/pkg/services/serviceaccounts/serviceaccounts.go b/pkg/services/serviceaccounts/serviceaccounts.go index ab8c35ba90c..7ba3b1b8fc3 100644 --- a/pkg/services/serviceaccounts/serviceaccounts.go +++ b/pkg/services/serviceaccounts/serviceaccounts.go @@ -15,7 +15,7 @@ type Service interface { type Store interface { CreateServiceAccount(ctx context.Context, saForm *CreateServiceAccountForm) (*ServiceAccountDTO, error) ListServiceAccounts(ctx context.Context, orgID, serviceAccountID int64) ([]*ServiceAccountDTO, error) - UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, saForm *UpdateServiceAccountForm) (*ServiceAccountDTO, error) + UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, saForm *UpdateServiceAccountForm) (*ServiceAccountProfileDTO, error) RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*ServiceAccountProfileDTO, error) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error UpgradeServiceAccounts(ctx context.Context) error diff --git a/pkg/services/serviceaccounts/tests/common.go b/pkg/services/serviceaccounts/tests/common.go index 8a177012bb9..0321741ce25 100644 --- a/pkg/services/serviceaccounts/tests/common.go +++ b/pkg/services/serviceaccounts/tests/common.go @@ -121,7 +121,7 @@ func (s *ServiceAccountsStoreMock) RetrieveServiceAccount(ctx context.Context, o func (s *ServiceAccountsStoreMock) UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, - saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { + saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) { s.Calls.UpdateServiceAccount = append(s.Calls.UpdateServiceAccount, []interface{}{ctx, orgID, serviceAccountID, saForm}) return nil, nil diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.tsx index 6494f31d77e..918c54102e0 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.tsx @@ -12,9 +12,10 @@ import { createServiceAccountToken, fetchACOptions, updateServiceAccount, + deleteServiceAccount, } from './state/actions'; import { ServiceAccountTokensTable } from './ServiceAccountTokensTable'; -import { getTimeZone, NavModel, OrgRole } from '@grafana/data'; +import { getTimeZone, NavModel } from '@grafana/data'; import { Button, VerticalGroup } from '@grafana/ui'; import { CreateTokenModal } from './CreateTokenModal'; import { contextSrv } from 'app/core/core'; @@ -44,6 +45,8 @@ const mapDispatchToProps = { loadServiceAccountTokens, createServiceAccountToken, deleteServiceAccountToken, + deleteServiceAccount, + updateServiceAccount, fetchACOptions, }; @@ -63,6 +66,8 @@ const ServiceAccountPageUnconnected = ({ loadServiceAccountTokens, createServiceAccountToken, deleteServiceAccountToken, + deleteServiceAccount, + updateServiceAccount, fetchACOptions, }: Props) => { const [isModalOpen, setIsModalOpen] = useState(false); @@ -90,12 +95,6 @@ const ServiceAccountPageUnconnected = ({ setNewToken(''); }; - const onRoleChange = (role: OrgRole, serviceAccount: ServiceAccountDTO) => { - const updatedServiceAccount = { ...serviceAccount, role: role }; - - updateServiceAccount(updatedServiceAccount); - }; - return ( @@ -104,21 +103,10 @@ const ServiceAccountPageUnconnected = ({ { - console.log(`not implemented`); - }} - onServiceAccountUpdate={() => { - console.log(`not implemented`); - }} - onServiceAccountDisable={() => { - console.log(`not implemented`); - }} - onServiceAccountEnable={() => { - console.log(`not implemented`); - }} - onRoleChange={onRoleChange} roleOptions={roleOptions} builtInRoles={builtInRoles} + updateServiceAccount={updateServiceAccount} + deleteServiceAccount={deleteServiceAccount} /> )} diff --git a/public/app/features/serviceaccounts/ServiceAccountProfile.tsx b/public/app/features/serviceaccounts/ServiceAccountProfile.tsx index 86c7b8c552a..98df4a391a9 100644 --- a/public/app/features/serviceaccounts/ServiceAccountProfile.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountProfile.tsx @@ -1,35 +1,27 @@ import React, { PureComponent, useRef, useState } from 'react'; import { Role, ServiceAccountDTO } from 'app/types'; import { css, cx } from '@emotion/css'; -import { config } from 'app/core/config'; -import { dateTimeFormat, GrafanaTheme, OrgRole, TimeZone } from '@grafana/data'; -import { Button, ConfirmButton, ConfirmModal, Input, LegacyInputStatus, stylesFactory } from '@grafana/ui'; +import { dateTimeFormat, GrafanaTheme2, OrgRole, TimeZone } from '@grafana/data'; +import { Button, ConfirmButton, ConfirmModal, Input, LegacyInputStatus, useStyles2 } from '@grafana/ui'; import { ServiceAccountRoleRow } from './ServiceAccountRoleRow'; interface Props { serviceAccount: ServiceAccountDTO; timeZone: TimeZone; - onServiceAccountUpdate: (serviceAccount: ServiceAccountDTO) => void; - onServiceAccountDelete: (serviceAccountId: number) => void; - onServiceAccountDisable: (serviceAccountId: number) => void; - onServiceAccountEnable: (serviceAccountId: number) => void; - - onRoleChange: (role: OrgRole, serviceAccount: ServiceAccountDTO) => void; roleOptions: Role[]; builtInRoles: Record; + deleteServiceAccount: (serviceAccountId: number) => void; + updateServiceAccount: (serviceAccount: ServiceAccountDTO) => void; } export function ServiceAccountProfile({ serviceAccount, timeZone, - onServiceAccountUpdate, - onServiceAccountDelete, - onServiceAccountDisable, - onServiceAccountEnable, - onRoleChange, roleOptions, builtInRoles, + deleteServiceAccount, + updateServiceAccount, }: Props) { const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDisableModal, setShowDisableModal] = useState(false); @@ -50,20 +42,27 @@ export function ServiceAccountProfile({ } }; - const handleServiceAccountDelete = () => onServiceAccountDelete(serviceAccount.id); - - const handleServiceAccountDisable = () => onServiceAccountDisable(serviceAccount.id); - - const handleServiceAccountEnable = () => onServiceAccountEnable(serviceAccount.id); - - const onServiceAccountNameChange = (newValue: string) => { - onServiceAccountUpdate({ - ...serviceAccount, - name: newValue, - }); + const handleServiceAccountDelete = () => { + deleteServiceAccount(serviceAccount.id); + }; + const handleServiceAccountDisable = () => { + updateServiceAccount({ ...serviceAccount, isDisabled: true }); + setShowDisableModal(false); }; - const styles = getStyles(config.theme); + const handleServiceAccountEnable = () => { + updateServiceAccount({ ...serviceAccount, isDisabled: false }); + }; + + const handleServiceAccountRoleChange = (role: OrgRole) => { + updateServiceAccount({ ...serviceAccount, role: role }); + }; + + const onServiceAccountNameChange = (newValue: string) => { + updateServiceAccount({ ...serviceAccount, name: newValue }); + }; + + const styles = useStyles2(getStyles); return ( <> @@ -84,7 +83,7 @@ export function ServiceAccountProfile({ @@ -98,26 +97,35 @@ export function ServiceAccountProfile({
<> - - {serviceAccount.isDisabled && ( - - )} - {!serviceAccount.isDisabled && ( + ) : ( <> - { +const getStyles = (theme: GrafanaTheme2) => { return { buttonRow: css` - margin-top: 0.8rem; + margin-top: ${theme.spacing(1.5)}; > * { - margin-right: 16px; + margin-right: ${theme.spacing(2)}; } `, }; -}); +}; interface ServiceAccountProfileRowProps { label: string; diff --git a/public/app/features/serviceaccounts/ServiceAccountRoleRow.tsx b/public/app/features/serviceaccounts/ServiceAccountRoleRow.tsx index 4fce6d47e59..851d4b1298d 100644 --- a/public/app/features/serviceaccounts/ServiceAccountRoleRow.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountRoleRow.tsx @@ -8,7 +8,7 @@ import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; interface Props { label: string; serviceAccount: ServiceAccountDTO; - onRoleChange: (role: OrgRole, serviceAccount: ServiceAccountDTO) => void; + onRoleChange: (role: OrgRole) => void; roleOptions: Role[]; builtInRoles: Record; } @@ -37,7 +37,7 @@ export class ServiceAccountRoleRow extends PureComponent { userId={serviceAccount.id} orgId={serviceAccount.orgId} builtInRole={serviceAccount.role} - onBuiltinRoleChange={(newRole) => onRoleChange(newRole, serviceAccount)} + onBuiltinRoleChange={onRoleChange} roleOptions={roleOptions} builtInRoles={builtInRoles} disabled={rolePickerDisabled} @@ -47,7 +47,7 @@ export class ServiceAccountRoleRow extends PureComponent { aria-label="Role" value={serviceAccount.role} disabled={!canUpdateRole} - onChange={(newRole) => onRoleChange(newRole, serviceAccount)} + onChange={onRoleChange} /> )} diff --git a/public/app/features/serviceaccounts/state/actions.ts b/public/app/features/serviceaccounts/state/actions.ts index e372e9c1272..f8dccf7d1d1 100644 --- a/public/app/features/serviceaccounts/state/actions.ts +++ b/public/app/features/serviceaccounts/state/actions.ts @@ -1,5 +1,5 @@ import { ApiKey, ServiceAccountDTO, ThunkResult } from '../../../types'; -import { getBackendSrv } from '@grafana/runtime'; +import { getBackendSrv, locationService } from '@grafana/runtime'; import { acOptionsLoaded, builtInRolesLoaded, @@ -90,8 +90,8 @@ export function loadServiceAccounts(): ThunkResult { export function updateServiceAccount(serviceAccount: ServiceAccountDTO): ThunkResult { return async (dispatch) => { - await getBackendSrv().patch(`/api/org/users/${serviceAccount.id}`, { role: serviceAccount.role }); - dispatch(loadServiceAccounts()); + const response = await getBackendSrv().patch(`${BASE_URL}/${serviceAccount.id}`, { ...serviceAccount }); + dispatch(serviceAccountLoaded(response)); }; } @@ -101,3 +101,10 @@ export function removeServiceAccount(serviceAccountId: number): ThunkResult { + return async (dispatch) => { + await getBackendSrv().delete(`${BASE_URL}/${serviceAccountId}`); + locationService.push('/org/serviceaccounts'); + }; +}