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 (
-
+