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 { useNavigate } from 'react-router-dom'; | ||||||
| import { Button, Dialog, styled } from '@mui/material'; | import { Button, Dialog, styled } from '@mui/material'; | ||||||
| import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg'; | 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 { | interface ICreateProjectDialogProps { | ||||||
|     open: boolean; |     open: boolean; | ||||||
| @ -41,11 +45,28 @@ const StyledProjectIcon = styled(ProjectIcon)(({ theme }) => ({ | |||||||
|     stroke: theme.palette.common.white, |     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 = ({ | export const CreateProjectDialog = ({ | ||||||
|     open, |     open, | ||||||
|     onClose, |     onClose, | ||||||
| }: ICreateProjectDialogProps) => { | }: ICreateProjectDialogProps) => { | ||||||
|     const { createProject, loading } = useProjectApi(); |     const { createProject, loading: creatingProject } = useProjectApi(); | ||||||
|     const { refetchUser } = useAuthUser(); |     const { refetchUser } = useAuthUser(); | ||||||
|     const { uiConfig } = useUiConfig(); |     const { uiConfig } = useUiConfig(); | ||||||
|     const { setToastData, setToastApiError } = useToast(); |     const { setToastData, setToastApiError } = useToast(); | ||||||
| @ -130,6 +151,14 @@ export const CreateProjectDialog = ({ | |||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  |     const { | ||||||
|  |         resourceLimitsEnabled, | ||||||
|  |         limit, | ||||||
|  |         currentValue, | ||||||
|  |         limitReached, | ||||||
|  |         loading: loadingLimit, | ||||||
|  |     } = useProjectLimit(); | ||||||
|  | 
 | ||||||
|     return ( |     return ( | ||||||
|         <StyledDialog open={open} onClose={onClose}> |         <StyledDialog open={open} onClose={onClose}> | ||||||
|             <FormTemplate |             <FormTemplate | ||||||
| @ -164,12 +193,26 @@ export const CreateProjectDialog = ({ | |||||||
|                     setProjectDesc={setProjectDesc} |                     setProjectDesc={setProjectDesc} | ||||||
|                     overrideDocumentation={setDocumentation} |                     overrideDocumentation={setDocumentation} | ||||||
|                     clearDocumentationOverride={clearDocumentationOverride} |                     clearDocumentationOverride={clearDocumentationOverride} | ||||||
|  |                     Limit={ | ||||||
|  |                         <ConditionallyRender | ||||||
|  |                             condition={resourceLimitsEnabled} | ||||||
|  |                             show={ | ||||||
|  |                                 <Limit | ||||||
|  |                                     name='projects' | ||||||
|  |                                     limit={limit} | ||||||
|  |                                     currentValue={currentValue} | ||||||
|  |                                 /> | ||||||
|  |                             } | ||||||
|  |                         /> | ||||||
|  |                     } | ||||||
|                 > |                 > | ||||||
|                     <Button onClick={onClose}>Cancel</Button> |                     <Button onClick={onClose}>Cancel</Button> | ||||||
|                     <CreateButton |                     <CreateButton | ||||||
|                         name='project' |                         name='project' | ||||||
|                         permission={CREATE_PROJECT} |                         permission={CREATE_PROJECT} | ||||||
|                         disabled={loading} |                         disabled={ | ||||||
|  |                             creatingProject || limitReached || loadingLimit | ||||||
|  |                         } | ||||||
|                         data-testid={CREATE_PROJECT_BTN} |                         data-testid={CREATE_PROJECT_BTN} | ||||||
|                     /> |                     /> | ||||||
|                 </NewProjectForm> |                 </NewProjectForm> | ||||||
|  | |||||||
| @ -25,6 +25,7 @@ import { | |||||||
| import { MultiSelectConfigButton } from './ConfigButtons/MultiSelectConfigButton'; | import { MultiSelectConfigButton } from './ConfigButtons/MultiSelectConfigButton'; | ||||||
| import { SingleSelectConfigButton } from './ConfigButtons/SingleSelectConfigButton'; | import { SingleSelectConfigButton } from './ConfigButtons/SingleSelectConfigButton'; | ||||||
| import { ChangeRequestTableConfigButton } from './ConfigButtons/ChangeRequestTableConfigButton'; | import { ChangeRequestTableConfigButton } from './ConfigButtons/ChangeRequestTableConfigButton'; | ||||||
|  | import { Box, styled } from '@mui/material'; | ||||||
| 
 | 
 | ||||||
| type FormProps = { | type FormProps = { | ||||||
|     projectId: string; |     projectId: string; | ||||||
| @ -51,6 +52,7 @@ type FormProps = { | |||||||
|     overrideDocumentation: (args: { text: string; icon: ReactNode }) => void; |     overrideDocumentation: (args: { text: string; icon: ReactNode }) => void; | ||||||
|     clearDocumentationOverride: () => void; |     clearDocumentationOverride: () => void; | ||||||
|     children?: React.ReactNode; |     children?: React.ReactNode; | ||||||
|  |     Limit?: React.ReactNode; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const PROJECT_NAME_INPUT = 'PROJECT_NAME_INPUT'; | 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> = ({ | export const NewProjectForm: React.FC<FormProps> = ({ | ||||||
|     children, |     children, | ||||||
|  |     Limit, | ||||||
|     handleSubmit, |     handleSubmit, | ||||||
|     projectName, |     projectName, | ||||||
|     projectDesc, |     projectDesc, | ||||||
| @ -324,6 +333,7 @@ export const NewProjectForm: React.FC<FormProps> = ({ | |||||||
|                     } |                     } | ||||||
|                 /> |                 /> | ||||||
|             </OptionButtons> |             </OptionButtons> | ||||||
|  |             <LimitContainer>{Limit}</LimitContainer> | ||||||
|             <FormActions>{children}</FormActions> |             <FormActions>{children}</FormActions> | ||||||
|         </StyledForm> |         </StyledForm> | ||||||
|     ); |     ); | ||||||
|  | |||||||
| @ -20,11 +20,11 @@ const setupApi = () => { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     testServerRoute(server, '/api/admin/projects', { |     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(); |     setupApi(); | ||||||
|     render(<ProjectListNew />, { |     render(<ProjectListNew />, { | ||||||
|         permissions: [{ permission: CREATE_PROJECT }], |         permissions: [{ permission: CREATE_PROJECT }], | ||||||
|  | |||||||
| @ -24,7 +24,6 @@ import { useProfile } from 'hooks/api/getters/useProfile/useProfile'; | |||||||
| import { groupProjects } from './group-projects'; | import { groupProjects } from './group-projects'; | ||||||
| import { ProjectGroup } from './ProjectGroup'; | import { ProjectGroup } from './ProjectGroup'; | ||||||
| import { CreateProjectDialog } from '../Project/CreateProject/NewCreateProjectForm/CreateProjectDialog'; | import { CreateProjectDialog } from '../Project/CreateProject/NewCreateProjectForm/CreateProjectDialog'; | ||||||
| import { useUiFlag } from 'hooks/useUiFlag'; |  | ||||||
| 
 | 
 | ||||||
| const StyledApiError = styled(ApiError)(({ theme }) => ({ | const StyledApiError = styled(ApiError)(({ theme }) => ({ | ||||||
|     maxWidth: '500px', |     maxWidth: '500px', | ||||||
| @ -54,7 +53,6 @@ const NAVIGATE_TO_CREATE_PROJECT = 'NAVIGATE_TO_CREATE_PROJECT'; | |||||||
| function resolveCreateButtonData( | function resolveCreateButtonData( | ||||||
|     isOss: boolean, |     isOss: boolean, | ||||||
|     hasAccess: boolean, |     hasAccess: boolean, | ||||||
|     limitReached: boolean, |  | ||||||
| ): ICreateButtonData { | ): ICreateButtonData { | ||||||
|     if (isOss) { |     if (isOss) { | ||||||
|         return { |         return { | ||||||
| @ -80,13 +78,6 @@ function resolveCreateButtonData( | |||||||
|             }, |             }, | ||||||
|             disabled: true, |             disabled: true, | ||||||
|         }; |         }; | ||||||
|     } else if (limitReached) { |  | ||||||
|         return { |  | ||||||
|             tooltip: { |  | ||||||
|                 title: 'Limit of allowed projects reached', |  | ||||||
|             }, |  | ||||||
|             disabled: true, |  | ||||||
|         }; |  | ||||||
|     } else { |     } else { | ||||||
|         return { |         return { | ||||||
|             tooltip: { title: 'Click to create a new project' }, |             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 }> = ({ | const ProjectCreationButton: FC<{ projectCount: number }> = ({ | ||||||
|     projectCount, |     projectCount, | ||||||
| }) => { | }) => { | ||||||
| @ -111,15 +95,9 @@ const ProjectCreationButton: FC<{ projectCount: number }> = ({ | |||||||
|     const { hasAccess } = useContext(AccessContext); |     const { hasAccess } = useContext(AccessContext); | ||||||
|     const { isOss, uiConfig, loading } = useUiConfig(); |     const { isOss, uiConfig, loading } = useUiConfig(); | ||||||
| 
 | 
 | ||||||
|     const limitReached = useProjectLimit( |  | ||||||
|         uiConfig.resourceLimits.projects, |  | ||||||
|         projectCount, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     const createButtonData = resolveCreateButtonData( |     const createButtonData = resolveCreateButtonData( | ||||||
|         isOss(), |         isOss(), | ||||||
|         hasAccess(CREATE_PROJECT), |         hasAccess(CREATE_PROJECT), | ||||||
|         limitReached, |  | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user