diff --git a/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.test.tsx b/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.test.tsx new file mode 100644 index 000000000000..387e1f0f83e7 --- /dev/null +++ b/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.test.tsx @@ -0,0 +1,64 @@ +import { render } from 'utils/testRenderer'; +import { screen, waitFor } from '@testing-library/react'; +import { testServerRoute, testServerSetup } from 'utils/testServer'; +import { CreateApiToken } from './CreateApiToken'; +import { + ADMIN, + CREATE_CLIENT_API_TOKEN, + CREATE_FRONTEND_API_TOKEN, +} from '@server/types/permissions'; + +const permissions = [ + { permission: CREATE_CLIENT_API_TOKEN }, + { permission: CREATE_FRONTEND_API_TOKEN }, + { permission: ADMIN }, +]; + +const server = testServerSetup(); + +const setupApi = (existingTokensCount: number) => { + testServerRoute(server, '/api/admin/ui-config', { + flags: { + resourceLimits: true, + }, + resourceLimits: { + apiTokens: 1, + }, + versionInfo: { + current: { enterprise: 'version' }, + }, + }); + + testServerRoute(server, '/api/admin/api-tokens', { + tokens: [...Array(existingTokensCount).keys()].map((_, i) => ({ + secret: `token${i}`, + })), + }); +}; + +test('Enabled new token button when limits, version and permission allow for it', async () => { + setupApi(0); + render(, { + permissions, + }); + + const button = await screen.findByText('Create token'); + expect(button).toBeDisabled(); + + await waitFor(async () => { + const button = await screen.findByText('Create token'); + expect(button).not.toBeDisabled(); + }); +}); + +test('Token limit reached', async () => { + setupApi(1); + render(, { + permissions, + }); + + await screen.findByText('You have reached the limit for API tokens'); + + const button = await screen.findByText('Create token'); + expect(button).toBeDisabled(); +}); diff --git a/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx b/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx index e5d30fd576ef..a820a61008d3 100644 --- a/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx +++ b/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx @@ -1,5 +1,8 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { styled } from '@mui/material'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { useUiFlag } from 'hooks/useUiFlag'; import FormTemplate from 'component/common/FormTemplate/FormTemplate'; import ApiTokenForm from '../ApiTokenForm/ApiTokenForm'; import { CreateButton } from 'component/common/CreateButton/CreateButton'; @@ -22,17 +25,45 @@ import { CREATE_CLIENT_API_TOKEN, CREATE_FRONTEND_API_TOKEN, } from '@server/types/permissions'; +import { Limit } from 'component/common/Limit/Limit'; const pageTitle = 'Create API token'; interface ICreateApiTokenProps { modal?: boolean; } + +const StyledLimit = styled(Limit)(({ theme }) => ({ + margin: theme.spacing(2, 0, 4), +})); + +const useApiTokenLimit = () => { + const resourceLimitsEnabled = useUiFlag('resourceLimits'); + const { tokens, loading: loadingTokens } = useApiTokens(); + const { uiConfig, loading: loadingConfig } = useUiConfig(); + const apiTokensLimit = uiConfig.resourceLimits.apiTokens; + + return { + resourceLimitsEnabled, + limit: apiTokensLimit, + currentValue: tokens.length, + limitReached: resourceLimitsEnabled && tokens.length >= apiTokensLimit, + loading: loadingConfig || loadingTokens, + }; +}; + export const CreateApiToken = ({ modal = false }: ICreateApiTokenProps) => { const { setToastApiError } = useToast(); const { uiConfig } = useUiConfig(); const navigate = useNavigate(); const [showConfirm, setShowConfirm] = useState(false); const [token, setToken] = useState(''); + const { + resourceLimitsEnabled, + limit, + currentValue, + limitReached, + loading: loadingLimit, + } = useApiTokenLimit(); const { getApiTokenPayload, @@ -50,7 +81,7 @@ export const CreateApiToken = ({ modal = false }: ICreateApiTokenProps) => { apiTokenTypes, } = useApiTokenForm(); - const { createToken, loading } = useApiTokensApi(); + const { createToken, loading: loadingCreateToken } = useApiTokensApi(); const { refetch } = useApiTokens(); usePageTitle(pageTitle); @@ -96,7 +127,7 @@ export const CreateApiToken = ({ modal = false }: ICreateApiTokenProps) => { return ( { CREATE_CLIENT_API_TOKEN, CREATE_FRONTEND_API_TOKEN, ]} + disabled={ + limitReached || loadingLimit || loadingCreateToken + } /> } > @@ -142,6 +176,17 @@ export const CreateApiToken = ({ modal = false }: ICreateApiTokenProps) => { environment={environment} setEnvironment={setEnvironment} /> + + } + /> void; -}> = ({ name, shortName, limit, currentValue, onClose }) => { + className?: string; +}> = ({ name, shortName, limit, currentValue, onClose, className }) => { const percentageLimit = Math.floor((currentValue / limit) * 100); const belowLimit = currentValue < limit; const threshold = 80; @@ -78,7 +79,7 @@ export const Limit: FC<{ } return ( - +