API keys: Remove state hideAPIkeys and refactor interface to IsDisabled (#64018)

* remove state and refactor interface to IsDisabled

* update docs and span

* Update pkg/services/apikey/apikey.go

Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com>

---------

Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com>
This commit is contained in:
Eric Leijonmarck 2023-03-03 16:12:34 +00:00 committed by GitHub
parent e6e8351ee9
commit ad4b053231
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 87 additions and 216 deletions

View File

@ -69,14 +69,3 @@ You can choose to migrate a single API key or all API keys. Note that when you m
1. Sign in to Grafana, hover your cursor over **Configuration** (the gear icon), and click **API Keys**. 1. Sign in to Grafana, hover your cursor over **Configuration** (the gear icon), and click **API Keys**.
1. Find the API Key you want to migrate. 1. Find the API Key you want to migrate.
1. Click **Migrate to service account**. 1. Click **Migrate to service account**.
### Revert service account token to API key
**Note:** This is undesired operation and should be used only in emergency situations.
It is possible to convert back service account token to API key. You can use the [Revert service account token to API key HTTP API]({{< relref "../../developers/http_api/create-api-tokens-for-org/#how-to-create-a-new-organization-and-an-api-token" >}}) for that.
**The revert will perform the following actions:**
1. Convert the given service account token back to API key
1. Delete the service account associated with the given key. **Make sure there are no other tokens associated with the service account, otherwise they all will be deleted.**

View File

@ -7,11 +7,12 @@ 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
GetApiKeyByName(ctx context.Context, query *GetByNameQuery) error GetApiKeyByName(ctx context.Context, query *GetByNameQuery) error
GetAPIKeyByHash(ctx context.Context, hash string) (*APIKey, error) GetAPIKeyByHash(ctx context.Context, hash string) (*APIKey, error)
UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64) error UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64) error
// IsDisabled returns true if the API key is not available for use.
IsDisabled(ctx context.Context, orgID int64) (bool, error)
} }

View File

@ -68,8 +68,15 @@ 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) // IsDisabled returns true if the apikey service is disabled for the given org.
// This is the case if the org has no apikeys.
func (s *Service) IsDisabled(ctx context.Context, orgID int64) (bool, error) {
apikeys, err := s.store.CountAPIKeys(ctx, orgID)
if err != nil {
return false, err
}
return apikeys == 0, nil
} }
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) { func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {

View File

@ -8,7 +8,7 @@ import (
type Service struct { type Service struct {
ExpectedError error ExpectedError error
ExpectedCount int64 ExpectedBool bool
ExpectedAPIKeys []*apikey.APIKey ExpectedAPIKeys []*apikey.APIKey
ExpectedAPIKey *apikey.APIKey ExpectedAPIKey *apikey.APIKey
} }
@ -20,9 +20,6 @@ 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
@ -44,3 +41,6 @@ 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.ExpectedError return s.ExpectedError
} }
func (s *Service) IsDisabled(ctx context.Context, orgID int64) (bool, error) {
return s.ExpectedBool, s.ExpectedError
}

View File

@ -79,15 +79,11 @@ func (s *ServiceImpl) getOrgAdminNode(c *contextmodel.ReqContext) (*navtree.NavL
}) })
} }
hideApiKeys, _, _ := s.kvStore.Get(c.Req.Context(), c.OrgID, "serviceaccounts", "hideApiKeys") disabled, err := s.apiKeyService.IsDisabled(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
} }
if hasAccess(ac.ReqOrgAdmin, ac.ApiKeyAccessEvaluator) && !disabled {
// Hide API keys if the global setting is set or if the org setting is set and there are no API keys
apiKeysHidden := hideApiKeys == "1" && apiKeys == 0
if hasAccess(ac.ReqOrgAdmin, ac.ApiKeyAccessEvaluator) && !apiKeysHidden {
configNodes = append(configNodes, &navtree.NavLink{ configNodes = append(configNodes, &navtree.NavLink{
Text: "API keys", Text: "API keys",
Id: "apikeys", Id: "apikeys",

View File

@ -40,7 +40,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
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
// Service account tokens // Service account tokens
@ -86,8 +85,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.Post("/hideApiKeys", auth(middleware.ReqOrgAdmin,
accesscontrol.EvalPermission(serviceaccounts.ActionCreate)), routing.Wrap(api.HideApiKeysTab))
serviceAccountsRoute.Post("/migrate", auth(middleware.ReqOrgAdmin, serviceAccountsRoute.Post("/migrate", auth(middleware.ReqOrgAdmin,
accesscontrol.EvalPermission(serviceaccounts.ActionCreate)), routing.Wrap(api.MigrateApiKeysToServiceAccounts)) accesscontrol.EvalPermission(serviceaccounts.ActionCreate)), routing.Wrap(api.MigrateApiKeysToServiceAccounts))
serviceAccountsRoute.Post("/migrate/:keyId", auth(middleware.ReqOrgAdmin, serviceAccountsRoute.Post("/migrate/:keyId", auth(middleware.ReqOrgAdmin,
@ -357,14 +354,6 @@ func (api *ServiceAccountsAPI) SearchOrgServiceAccountsWithPaging(c *contextmode
return response.JSON(http.StatusOK, serviceAccountSearch) return response.JSON(http.StatusOK, serviceAccountSearch)
} }
// POST /api/serviceaccounts/hideapikeys
func (api *ServiceAccountsAPI) HideApiKeysTab(ctx *contextmodel.ReqContext) response.Response {
if err := api.service.HideApiKeysTab(ctx.Req.Context(), ctx.OrgID); err != nil {
return response.Error(http.StatusInternalServerError, "Internal server error", err)
}
return response.Success("API keys hidden")
}
// POST /api/serviceaccounts/migrate // POST /api/serviceaccounts/migrate
func (api *ServiceAccountsAPI) MigrateApiKeysToServiceAccounts(ctx *contextmodel.ReqContext) response.Response { func (api *ServiceAccountsAPI) MigrateApiKeysToServiceAccounts(ctx *contextmodel.ReqContext) response.Response {
if err := api.service.MigrateApiKeysToServiceAccounts(ctx.Req.Context(), ctx.OrgID); err != nil { if err := api.service.MigrateApiKeysToServiceAccounts(ctx.Req.Context(), ctx.OrgID); err != nil {

View File

@ -364,13 +364,6 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context,
return searchResult, nil return searchResult, nil
} }
func (s *ServiceAccountsStoreImpl) HideApiKeysTab(ctx context.Context, orgId int64) error {
if err := s.kvStore.Set(ctx, orgId, "serviceaccounts", "hideApiKeys", "1"); err != nil {
s.log.Error("Failed to hide API keys tab", err)
}
return nil
}
func (s *ServiceAccountsStoreImpl) MigrateApiKeysToServiceAccounts(ctx context.Context, orgId int64) error { func (s *ServiceAccountsStoreImpl) MigrateApiKeysToServiceAccounts(ctx context.Context, orgId int64) error {
basicKeys, err := s.apiKeyService.GetAllAPIKeys(ctx, orgId) basicKeys, err := s.apiKeyService.GetAllAPIKeys(ctx, orgId)
if err != nil { if err != nil {

View File

@ -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) HideApiKeysTab(ctx context.Context, orgID int64) error {
if err := validOrgID(orgID); err != nil {
return err
}
return sa.store.HideApiKeysTab(ctx, orgID)
}
func (sa *ServiceAccountsService) MigrateApiKey(ctx context.Context, orgID, keyID int64) error { func (sa *ServiceAccountsService) MigrateApiKey(ctx context.Context, orgID, keyID int64) error {
if err := validOrgID(orgID); err != nil { if err := validOrgID(orgID); err != nil {
return err return err

View File

@ -59,11 +59,6 @@ func (f *FakeServiceAccountStore) DeleteServiceAccount(ctx context.Context, orgI
return f.ExpectedError return f.ExpectedError
} }
// HideApiKeysTab is a fake hiding the api keys tab.
func (f *FakeServiceAccountStore) HideApiKeysTab(ctx context.Context, orgID int64) error {
return f.ExpectedError
}
// MigrateApiKeysToServiceAccounts is a fake migrating api keys to service accounts. // MigrateApiKeysToServiceAccounts is a fake migrating api keys to service accounts.
func (f *FakeServiceAccountStore) MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) error { func (f *FakeServiceAccountStore) MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) error {
return f.ExpectedError return f.ExpectedError

View File

@ -7,17 +7,6 @@ import (
"github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts"
) )
/*
Store is the database store for service accounts.
migration from apikeys to service accounts:
HideApiKeyTab is used to hide the api key tab in the UI.
MigrateApiKeysToServiceAccounts migrates all API keys to service accounts.
MigrateApiKey migrates a single API key to a service account.
// only used for interal api calls
RevertApiKey reverts a single service account to an API key.
*/
type store interface { type store interface {
CreateServiceAccount(ctx context.Context, orgID int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) CreateServiceAccount(ctx context.Context, orgID int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error)
SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error)
@ -26,7 +15,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
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
ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error) ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error)

View File

@ -1,52 +0,0 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, ConfirmModal, useStyles2, Button } from '@grafana/ui';
interface Props {
onHideApiKeys: () => void;
apikeys: number;
}
export const APIKeysMigratedCard = ({ onHideApiKeys, apikeys }: Props): JSX.Element => {
const [isModalOpen, setIsModalOpen] = useState(false);
const styles = useStyles2(getStyles);
return (
<Alert title="If you see any API keys please migrate them to Service account tokens." severity="info">
<div className={styles.text}>
Migrated API keys are safe and continue working as they used to. You can find them inside the respective service
account.
</div>
<div className={styles.actionRow}>
<Button className={styles.actionButton} onClick={() => setIsModalOpen(true)} disabled={apikeys !== 0}>
Hide API keys page
</Button>
<ConfirmModal
title={'Hide API Keys page'}
isOpen={isModalOpen}
body={'Did you want to hide the API keys page?'}
confirmText={'Yes, hide API keys page.'}
onConfirm={onHideApiKeys}
onDismiss={() => setIsModalOpen(false)}
confirmButtonVariant="primary"
/>
<a href="org/serviceaccounts">View service accounts page</a>
</div>
</Alert>
);
};
export const getStyles = (theme: GrafanaTheme2) => ({
text: css`
margin-bottom: ${theme.spacing(2)};
`,
actionRow: css`
display: flex;
align-items: center;
`,
actionButton: css`
margin-right: ${theme.spacing(2)};
`,
});

View File

@ -1,21 +0,0 @@
import { FC, useCallback, useState } from 'react';
interface Api {
isAdding: boolean;
toggleIsAdding: () => void;
}
interface Props {
children: (props: Api) => JSX.Element;
}
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 toggleIsAdding = useCallback(() => {
setIsAdding(!isAdding);
}, [isAdding]);
return children({ isAdding, toggleIsAdding });
};

View File

@ -29,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 hideApiKeysMock = jest.fn();
const props: Props = { const props: Props = {
apiKeys: [] as ApiKey[], apiKeys: [] as ApiKey[],
searchQuery: '', searchQuery: '',
@ -39,7 +38,6 @@ const setup = (propOverrides: Partial<Props>) => {
setSearchQuery: setSearchQueryMock, setSearchQuery: setSearchQueryMock,
migrateApiKey: migrateApiKeyMock, migrateApiKey: migrateApiKeyMock,
migrateAll: migrateAllMock, migrateAll: migrateAllMock,
hideApiKeys: hideApiKeysMock,
apiKeysCount: 0, apiKeysCount: 0,
timeZone: 'utc', timeZone: 'utc',
includeExpired: false, includeExpired: false,

View File

@ -9,19 +9,10 @@ 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, StoreState } from 'app/types'; import { AccessControlAction, ApiKey, StoreState } from 'app/types';
import { APIKeysMigratedCard } from './APIKeysMigratedCard';
import { ApiKeysActionBar } from './ApiKeysActionBar'; import { ApiKeysActionBar } from './ApiKeysActionBar';
import { ApiKeysController } from './ApiKeysController';
import { ApiKeysTable } from './ApiKeysTable'; import { ApiKeysTable } from './ApiKeysTable';
import { MigrateToServiceAccountsCard } from './MigrateToServiceAccountsCard'; import { MigrateToServiceAccountsCard } from './MigrateToServiceAccountsCard';
import { import { deleteApiKey, migrateApiKey, migrateAll, loadApiKeys, toggleIncludeExpired } from './state/actions';
deleteApiKey,
migrateApiKey,
migrateAll,
loadApiKeys,
toggleIncludeExpired,
hideApiKeys,
} from './state/actions';
import { setSearchQuery } from './state/reducers'; import { setSearchQuery } from './state/reducers';
import { getApiKeys, getApiKeysCount, getIncludeExpired, getIncludeExpiredDisabled } from './state/selectors'; import { getApiKeys, getApiKeysCount, getIncludeExpired, getIncludeExpiredDisabled } from './state/selectors';
@ -51,7 +42,6 @@ const mapDispatchToProps = {
migrateAll, migrateAll,
setSearchQuery, setSearchQuery,
toggleIncludeExpired, toggleIncludeExpired,
hideApiKeys,
}; };
const connector = connect(mapStateToProps, mapDispatchToProps); const connector = connect(mapStateToProps, mapDispatchToProps);
@ -97,9 +87,9 @@ export class ApiKeysPageUnconnected extends PureComponent<Props, State> {
this.props.toggleIncludeExpired(); this.props.toggleIncludeExpired();
}; };
onHideApiKeys = async () => { onMigrateApiKeys = async () => {
try { try {
await this.props.hideApiKeys(); this.onMigrateAll();
let serviceAccountsUrl = '/org/serviceaccounts'; let serviceAccountsUrl = '/org/serviceaccounts';
locationService.push(serviceAccountsUrl); locationService.push(serviceAccountsUrl);
window.location.reload(); window.location.reload();
@ -128,42 +118,33 @@ export class ApiKeysPageUnconnected extends PureComponent<Props, State> {
); );
} }
const showTable = apiKeysCount > 0;
return ( return (
<Page {...defaultPageProps}> <Page {...defaultPageProps}>
<Page.Contents isLoading={false}> <Page.Contents isLoading={false}>
<ApiKeysController> <>
{({}) => { <MigrateToServiceAccountsCard onMigrate={this.onMigrateApiKeys} apikeysCount={apiKeysCount} />
const showTable = apiKeysCount > 0; {showTable ? (
return ( <ApiKeysActionBar
<> searchQuery={searchQuery}
{apiKeysCount !== 0 && <MigrateToServiceAccountsCard onMigrate={this.onMigrateAll} />} disabled={!canCreate}
{apiKeysCount === 0 && ( onSearchChange={this.onSearchQueryChange}
<APIKeysMigratedCard onHideApiKeys={this.onHideApiKeys} apikeys={apiKeysCount} /> />
)} ) : null}
{showTable ? ( {showTable ? (
<ApiKeysActionBar <VerticalGroup>
searchQuery={searchQuery} <InlineField disabled={includeExpiredDisabled} label="Include expired keys">
disabled={!canCreate} <InlineSwitch id="showExpired" value={includeExpired} onChange={this.onIncludeExpiredChange} />
onSearchChange={this.onSearchQueryChange} </InlineField>
/> <ApiKeysTable
) : null} apiKeys={apiKeys}
{showTable ? ( timeZone={timeZone}
<VerticalGroup> onMigrate={this.onMigrateApiKey}
<InlineField disabled={includeExpiredDisabled} label="Include expired keys"> onDelete={this.onDeleteApiKey}
<InlineSwitch id="showExpired" value={includeExpired} onChange={this.onIncludeExpiredChange} /> />
</InlineField> </VerticalGroup>
<ApiKeysTable ) : null}
apiKeys={apiKeys} </>
timeZone={timeZone}
onMigrate={this.onMigrateApiKey}
onDelete={this.onDeleteApiKey}
/>
</VerticalGroup>
) : null}
</>
);
}}
</ApiKeysController>
</Page.Contents> </Page.Contents>
</Page> </Page>
); );

View File

@ -6,10 +6,11 @@ import { Alert, Button, ConfirmModal, useStyles2 } from '@grafana/ui';
interface Props { interface Props {
onMigrate: () => void; onMigrate: () => void;
apikeysCount: number;
disabled?: boolean; disabled?: boolean;
} }
export const MigrateToServiceAccountsCard = ({ onMigrate, disabled }: Props): JSX.Element => { export const MigrateToServiceAccountsCard = ({ onMigrate, apikeysCount, disabled }: Props): JSX.Element => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@ -23,30 +24,49 @@ export const MigrateToServiceAccountsCard = ({ onMigrate, disabled }: Props): JS
Find out more about the migration here. Find out more about the migration here.
</a> </a>
); );
const migrationBoxDesc = <span>Are you sure you want to migrate all API keys to service accounts? {docsLink}</span>; const migrationBoxDesc = (
<span>
Are you sure you want to migrate all API keys to service accounts? {docsLink}
<br>This hides the API keys tab.</br>
</span>
);
return ( return (
<Alert title="Switch from API keys to service accounts" severity="warning"> <>
<div className={styles.text}> {apikeysCount > 0 && (
We will soon deprecate API keys. Each API key will be migrated into a service account with a token and will <Alert title="Switch from API keys to service accounts" severity="warning">
continue to work as they were. We encourage you to migrate your API keys to service accounts now. {docsLink} <div className={styles.text}>
</div> We will soon deprecate API keys. Each API key will be migrated into a service account with a token and will
<div className={styles.actionRow}> continue to work as they were. We encourage you to migrate your API keys to service accounts now. {docsLink}
<Button className={styles.actionButton} onClick={() => setIsModalOpen(true)}> </div>
Migrate all service accounts <div className={styles.actionRow}>
</Button> <Button className={styles.actionButton} onClick={() => setIsModalOpen(true)}>
<ConfirmModal Migrate all service accounts
title={'Migrate API keys to service accounts'} </Button>
isOpen={isModalOpen} <ConfirmModal
body={migrationBoxDesc} title={'Migrate API keys to service accounts'}
confirmText={'Yes, migrate now'} isOpen={isModalOpen}
onConfirm={onMigrate} body={migrationBoxDesc}
onDismiss={() => setIsModalOpen(false)} confirmText={'Yes, migrate now'}
confirmVariant="primary" onConfirm={onMigrate}
confirmButtonVariant="primary" onDismiss={() => setIsModalOpen(false)}
/> confirmVariant="primary"
</div> confirmButtonVariant="primary"
</Alert> />
</div>
</Alert>
)}
{apikeysCount === 0 && (
<>
<Alert title="No API keys found" severity="warning">
<div className={styles.text}>
No API keys were found. If you reload the browser, this tab will be not available. If any API keys are
created, this tab will appear again.
</div>
</Alert>
</>
)}
</>
); );
}; };

View File

@ -42,12 +42,6 @@ export function migrateAll(): ThunkResult<void> {
}; };
} }
export function hideApiKeys(): ThunkResult<void> {
return async (dispatch) => {
await getBackendSrv().post('/api/serviceaccounts/hideApiKeys');
};
}
export function toggleIncludeExpired(): ThunkResult<void> { export function toggleIncludeExpired(): ThunkResult<void> {
return (dispatch) => { return (dispatch) => {
dispatch(includeExpiredToggled()); dispatch(includeExpiredToggled());