mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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:
		
							parent
							
								
									f1b375876f
								
							
						
					
					
						commit
						e7627becec
					
				| @ -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(); | ||||
| }); | ||||
| @ -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} | ||||
|  | ||||
| @ -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} | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user