1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-28 00:06:53 +01:00

feat: API Tokens limit - UI (#7561)

When approaching limit or limit reached for the number of API tokens, we
show a corresponding message.
This commit is contained in:
Tymoteusz Czech 2024-07-12 14:44:46 +02:00 committed by GitHub
parent f1b375876f
commit e7627becec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 114 additions and 4 deletions

View File

@ -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(<CreateApiToken />, {
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(<CreateApiToken />, {
permissions,
});
await screen.findByText('You have reached the limit for API tokens');
const button = await screen.findByText('Create token');
expect(button).toBeDisabled();
});

View File

@ -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 (
<FormTemplate
loading={loading}
loading={loadingCreateToken}
title={pageTitle}
modal={modal}
description="Unleash SDKs use API tokens to authenticate to the Unleash API. Client SDKs need a token with 'client privileges', which allows them to fetch feature flag configurations and post usage metrics."
@ -116,6 +147,9 @@ export const CreateApiToken = ({ modal = false }: ICreateApiTokenProps) => {
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}
/>
<ConditionallyRender
condition={resourceLimitsEnabled}
show={
<StyledLimit
name='API tokens'
shortName='tokens'
currentValue={currentValue}
limit={limit}
/>
}
/>
</ApiTokenForm>
<ConfirmToken
open={showConfirm}

View File

@ -68,7 +68,8 @@ export const Limit: FC<{
limit: number;
currentValue: number;
onClose?: () => 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 (
<StyledBox>
<StyledBox className={className}>
<Header>
<ConditionallyRender
condition={belowLimit}