mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: project limits ui (#7558)
This commit is contained in:
		
							parent
							
								
									46b1eedcc7
								
							
						
					
					
						commit
						2aea6e688c
					
				| @ -0,0 +1,54 @@ | ||||
| import { render } from 'utils/testRenderer'; | ||||
| import { screen, waitFor } from '@testing-library/react'; | ||||
| import { testServerRoute, testServerSetup } from 'utils/testServer'; | ||||
| import { CreateProjectDialog } from './CreateProjectDialog'; | ||||
| import { CREATE_PROJECT } from '../../../../providers/AccessProvider/permissions'; | ||||
| 
 | ||||
| const server = testServerSetup(); | ||||
| 
 | ||||
| const setupApi = (existingProjectsCount: number) => { | ||||
|     testServerRoute(server, '/api/admin/ui-config', { | ||||
|         flags: { | ||||
|             resourceLimits: true, | ||||
|         }, | ||||
|         resourceLimits: { | ||||
|             projects: 1, | ||||
|         }, | ||||
|         versionInfo: { | ||||
|             current: { enterprise: 'version' }, | ||||
|         }, | ||||
|     }); | ||||
| 
 | ||||
|     testServerRoute(server, '/api/admin/projects', { | ||||
|         projects: [...Array(existingProjectsCount).keys()].map((_, i) => ({ | ||||
|             name: `project${i}`, | ||||
|         })), | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
| test('Enabled new project button when limits, version and permission allow for it', async () => { | ||||
|     setupApi(0); | ||||
|     render(<CreateProjectDialog open={true} onClose={() => {}} />, { | ||||
|         permissions: [{ permission: CREATE_PROJECT }], | ||||
|     }); | ||||
| 
 | ||||
|     const button = await screen.findByText('Create project'); | ||||
|     expect(button).toBeDisabled(); | ||||
| 
 | ||||
|     await waitFor(async () => { | ||||
|         const button = await screen.findByText('Create project'); | ||||
|         expect(button).not.toBeDisabled(); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| test('Project limit reached', async () => { | ||||
|     setupApi(1); | ||||
|     render(<CreateProjectDialog open={true} onClose={() => {}} />, { | ||||
|         permissions: [{ permission: CREATE_PROJECT }], | ||||
|     }); | ||||
| 
 | ||||
|     await screen.findByText('You have reached the limit for projects'); | ||||
| 
 | ||||
|     const button = await screen.findByText('Create project'); | ||||
|     expect(button).toBeDisabled(); | ||||
| }); | ||||
| @ -15,6 +15,10 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import { Button, Dialog, styled } from '@mui/material'; | ||||
| import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| import useProjects from 'hooks/api/getters/useProjects/useProjects'; | ||||
| import { Limit } from 'component/common/Limit/Limit'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| 
 | ||||
| interface ICreateProjectDialogProps { | ||||
|     open: boolean; | ||||
| @ -41,11 +45,28 @@ const StyledProjectIcon = styled(ProjectIcon)(({ theme }) => ({ | ||||
|     stroke: theme.palette.common.white, | ||||
| })); | ||||
| 
 | ||||
| const useProjectLimit = () => { | ||||
|     const resourceLimitsEnabled = useUiFlag('resourceLimits'); | ||||
|     const { projects, loading: loadingProjects } = useProjects(); | ||||
|     const { uiConfig, loading: loadingConfig } = useUiConfig(); | ||||
|     const projectsLimit = uiConfig.resourceLimits?.projects; | ||||
|     const limitReached = | ||||
|         resourceLimitsEnabled && projects.length >= projectsLimit; | ||||
| 
 | ||||
|     return { | ||||
|         resourceLimitsEnabled, | ||||
|         limit: projectsLimit, | ||||
|         currentValue: projects.length, | ||||
|         limitReached, | ||||
|         loading: loadingConfig || loadingProjects, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| export const CreateProjectDialog = ({ | ||||
|     open, | ||||
|     onClose, | ||||
| }: ICreateProjectDialogProps) => { | ||||
|     const { createProject, loading } = useProjectApi(); | ||||
|     const { createProject, loading: creatingProject } = useProjectApi(); | ||||
|     const { refetchUser } = useAuthUser(); | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
| @ -130,6 +151,14 @@ export const CreateProjectDialog = ({ | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const { | ||||
|         resourceLimitsEnabled, | ||||
|         limit, | ||||
|         currentValue, | ||||
|         limitReached, | ||||
|         loading: loadingLimit, | ||||
|     } = useProjectLimit(); | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledDialog open={open} onClose={onClose}> | ||||
|             <FormTemplate | ||||
| @ -164,12 +193,26 @@ export const CreateProjectDialog = ({ | ||||
|                     setProjectDesc={setProjectDesc} | ||||
|                     overrideDocumentation={setDocumentation} | ||||
|                     clearDocumentationOverride={clearDocumentationOverride} | ||||
|                     Limit={ | ||||
|                         <ConditionallyRender | ||||
|                             condition={resourceLimitsEnabled} | ||||
|                             show={ | ||||
|                                 <Limit | ||||
|                                     name='projects' | ||||
|                                     limit={limit} | ||||
|                                     currentValue={currentValue} | ||||
|                                 /> | ||||
|                             } | ||||
|                         /> | ||||
|                     } | ||||
|                 > | ||||
|                     <Button onClick={onClose}>Cancel</Button> | ||||
|                     <CreateButton | ||||
|                         name='project' | ||||
|                         permission={CREATE_PROJECT} | ||||
|                         disabled={loading} | ||||
|                         disabled={ | ||||
|                             creatingProject || limitReached || loadingLimit | ||||
|                         } | ||||
|                         data-testid={CREATE_PROJECT_BTN} | ||||
|                     /> | ||||
|                 </NewProjectForm> | ||||
|  | ||||
| @ -25,6 +25,7 @@ import { | ||||
| import { MultiSelectConfigButton } from './ConfigButtons/MultiSelectConfigButton'; | ||||
| import { SingleSelectConfigButton } from './ConfigButtons/SingleSelectConfigButton'; | ||||
| import { ChangeRequestTableConfigButton } from './ConfigButtons/ChangeRequestTableConfigButton'; | ||||
| import { Box, styled } from '@mui/material'; | ||||
| 
 | ||||
| type FormProps = { | ||||
|     projectId: string; | ||||
| @ -51,6 +52,7 @@ type FormProps = { | ||||
|     overrideDocumentation: (args: { text: string; icon: ReactNode }) => void; | ||||
|     clearDocumentationOverride: () => void; | ||||
|     children?: React.ReactNode; | ||||
|     Limit?: React.ReactNode; | ||||
| }; | ||||
| 
 | ||||
| const PROJECT_NAME_INPUT = 'PROJECT_NAME_INPUT'; | ||||
| @ -104,8 +106,15 @@ const configButtonData = { | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| const LimitContainer = styled(Box)(({ theme }) => ({ | ||||
|     '&:has(*)': { | ||||
|         padding: theme.spacing(4, 6, 0, 6), | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| export const NewProjectForm: React.FC<FormProps> = ({ | ||||
|     children, | ||||
|     Limit, | ||||
|     handleSubmit, | ||||
|     projectName, | ||||
|     projectDesc, | ||||
| @ -324,6 +333,7 @@ export const NewProjectForm: React.FC<FormProps> = ({ | ||||
|                     } | ||||
|                 /> | ||||
|             </OptionButtons> | ||||
|             <LimitContainer>{Limit}</LimitContainer> | ||||
|             <FormActions>{children}</FormActions> | ||||
|         </StyledForm> | ||||
|     ); | ||||
|  | ||||
| @ -20,11 +20,11 @@ const setupApi = () => { | ||||
|     }); | ||||
| 
 | ||||
|     testServerRoute(server, '/api/admin/projects', { | ||||
|         projects: [], | ||||
|         projects: [{ name: 'existing' }], | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
| test('Enabled new project button when limits, version and permission allow for it', async () => { | ||||
| test('Enabled new project button when version and permission allow for it and limit is reached', async () => { | ||||
|     setupApi(); | ||||
|     render(<ProjectListNew />, { | ||||
|         permissions: [{ permission: CREATE_PROJECT }], | ||||
|  | ||||
| @ -24,7 +24,6 @@ import { useProfile } from 'hooks/api/getters/useProfile/useProfile'; | ||||
| import { groupProjects } from './group-projects'; | ||||
| import { ProjectGroup } from './ProjectGroup'; | ||||
| import { CreateProjectDialog } from '../Project/CreateProject/NewCreateProjectForm/CreateProjectDialog'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| 
 | ||||
| const StyledApiError = styled(ApiError)(({ theme }) => ({ | ||||
|     maxWidth: '500px', | ||||
| @ -54,7 +53,6 @@ const NAVIGATE_TO_CREATE_PROJECT = 'NAVIGATE_TO_CREATE_PROJECT'; | ||||
| function resolveCreateButtonData( | ||||
|     isOss: boolean, | ||||
|     hasAccess: boolean, | ||||
|     limitReached: boolean, | ||||
| ): ICreateButtonData { | ||||
|     if (isOss) { | ||||
|         return { | ||||
| @ -80,13 +78,6 @@ function resolveCreateButtonData( | ||||
|             }, | ||||
|             disabled: true, | ||||
|         }; | ||||
|     } else if (limitReached) { | ||||
|         return { | ||||
|             tooltip: { | ||||
|                 title: 'Limit of allowed projects reached', | ||||
|             }, | ||||
|             disabled: true, | ||||
|         }; | ||||
|     } else { | ||||
|         return { | ||||
|             tooltip: { title: 'Click to create a new project' }, | ||||
| @ -95,13 +86,6 @@ function resolveCreateButtonData( | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| const useProjectLimit = (projectsLimit: number, projectCount: number) => { | ||||
|     const resourceLimitsEnabled = useUiFlag('resourceLimits'); | ||||
|     const limitReached = resourceLimitsEnabled && projectCount >= projectsLimit; | ||||
| 
 | ||||
|     return limitReached; | ||||
| }; | ||||
| 
 | ||||
| const ProjectCreationButton: FC<{ projectCount: number }> = ({ | ||||
|     projectCount, | ||||
| }) => { | ||||
| @ -111,15 +95,9 @@ const ProjectCreationButton: FC<{ projectCount: number }> = ({ | ||||
|     const { hasAccess } = useContext(AccessContext); | ||||
|     const { isOss, uiConfig, loading } = useUiConfig(); | ||||
| 
 | ||||
|     const limitReached = useProjectLimit( | ||||
|         uiConfig.resourceLimits.projects, | ||||
|         projectCount, | ||||
|     ); | ||||
| 
 | ||||
|     const createButtonData = resolveCreateButtonData( | ||||
|         isOss(), | ||||
|         hasAccess(CREATE_PROJECT), | ||||
|         limitReached, | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user