diff --git a/frontend/src/component/common/ApiTokenTable/CreateApiTokenButton/CreateApiTokenButton.test.tsx b/frontend/src/component/common/ApiTokenTable/CreateApiTokenButton/CreateApiTokenButton.test.tsx new file mode 100644 index 000000000000..efee510fe430 --- /dev/null +++ b/frontend/src/component/common/ApiTokenTable/CreateApiTokenButton/CreateApiTokenButton.test.tsx @@ -0,0 +1,67 @@ +import { screen, waitFor } from '@testing-library/react'; +import { render } from 'utils/testRenderer'; +import { testServerRoute, testServerSetup } from 'utils/testServer'; +import { CreateApiTokenButton } from './CreateApiTokenButton'; +import { CREATE_PROJECT_API_TOKEN } from 'component/providers/AccessProvider/permissions'; + +const server = testServerSetup(); + +const setupApi = ({ + apiTokenCount, + apiTokenLimit, +}: { apiTokenCount: number; apiTokenLimit: number }) => { + testServerRoute(server, '/api/admin/ui-config', { + flags: { + resourceLimits: true, + }, + resourceLimits: { + apiTokens: apiTokenLimit, + }, + }); + + testServerRoute(server, '/api/admin/api-tokens', { + tokens: Array.from({ length: apiTokenCount }).map((_, i) => ({ + secret: 'super-secret', + tokenName: `token—name-${i}`, + type: 'client', + })), + }); +}; + +test('should allow you to create API tokens when there are fewer apiTokens than the limit', async () => { + setupApi({ apiTokenLimit: 3, apiTokenCount: 2 }); + + render( + , + { + permissions: [{ permission: CREATE_PROJECT_API_TOKEN }], + }, + ); + + await waitFor(async () => { + const button = await screen.findByRole('button'); + expect(button).not.toBeDisabled(); + }); +}); + +test('should not allow you to create API tokens when you have reached the limit', async () => { + setupApi({ apiTokenLimit: 3, apiTokenCount: 3 }); + + render( + , + { + permissions: [{ permission: CREATE_PROJECT_API_TOKEN }], + }, + ); + + await waitFor(async () => { + const button = await screen.findByRole('button'); + expect(button).toBeDisabled(); + }); +}); diff --git a/frontend/src/component/common/ApiTokenTable/CreateApiTokenButton/CreateApiTokenButton.tsx b/frontend/src/component/common/ApiTokenTable/CreateApiTokenButton/CreateApiTokenButton.tsx index dd9e40dd8154..f0a5fdd76e1f 100644 --- a/frontend/src/component/common/ApiTokenTable/CreateApiTokenButton/CreateApiTokenButton.tsx +++ b/frontend/src/component/common/ApiTokenTable/CreateApiTokenButton/CreateApiTokenButton.tsx @@ -2,18 +2,41 @@ import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton import { CREATE_API_TOKEN_BUTTON } from 'utils/testIds'; import { useNavigate } from 'react-router-dom'; import Add from '@mui/icons-material/Add'; +import { useApiTokens } from 'hooks/api/getters/useApiTokens/useApiTokens'; +import { useUiFlag } from 'hooks/useUiFlag'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; interface ICreateApiTokenButton { path: string; permission: string | string[]; project?: string; } +const useApiTokenLimit = (apiTokenLimit: number, apiTokenCount: number) => { + const resourceLimitsEnabled = useUiFlag('resourceLimits'); + const limitReached = + resourceLimitsEnabled && apiTokenCount >= apiTokenLimit; + + return { + limitReached, + limitMessage: limitReached + ? `You have reached the limit of ${apiTokenLimit} API tokens` + : undefined, + }; +}; + export const CreateApiTokenButton = ({ path, permission, project, }: ICreateApiTokenButton) => { const navigate = useNavigate(); + const { tokens, loading } = useApiTokens(); + const { uiConfig } = useUiConfig(); + + const { limitReached, limitMessage } = useApiTokenLimit( + uiConfig.resourceLimits.apiTokens, + tokens.length, + ); return ( New API token diff --git a/frontend/src/hooks/api/getters/useUiConfig/defaultValue.tsx b/frontend/src/hooks/api/getters/useUiConfig/defaultValue.tsx index b81dcbd045e8..903af08f0a2d 100644 --- a/frontend/src/hooks/api/getters/useUiConfig/defaultValue.tsx +++ b/frontend/src/hooks/api/getters/useUiConfig/defaultValue.tsx @@ -42,5 +42,6 @@ export const defaultValue: IUiConfig = { constraintValues: 250, projects: 500, segments: 300, + apiTokens: 2000, }, }; diff --git a/frontend/src/openapi/models/resourceLimitsSchema.ts b/frontend/src/openapi/models/resourceLimitsSchema.ts index 5a5770852def..a7fb36e34c1b 100644 --- a/frontend/src/openapi/models/resourceLimitsSchema.ts +++ b/frontend/src/openapi/models/resourceLimitsSchema.ts @@ -32,6 +32,13 @@ export interface ResourceLimitsSchema { constraintValues: number; /** The maximum number of projects allowed. */ projects: number; - /** The maximum number of segment allowed. */ + /** The maximum number of segments allowed. */ segments: number; + /** The maximum number of SDK and admin API tokens you can have at + * the same time. This limit applies only to server-side and + * client-side SDK tokens and to admin tokens. Personal access + * tokens are not subject to this limit. The limit applies to the + * total number of tokens across all projects in your + * organization. */ + apiTokens: number; }