From 2aea6e688c054b0437bb41dcc205f994f7c73d34 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Tue, 9 Jul 2024 11:04:23 +0200 Subject: [PATCH] feat: project limits ui (#7558) --- .../CreateProjectDialog.test.tsx | 54 +++++++++++++++++++ .../CreateProjectDialog.tsx | 47 +++++++++++++++- .../NewCreateProjectForm/NewProjectForm.tsx | 10 ++++ .../project/ProjectList/ProjectList.test.tsx | 4 +- .../project/ProjectList/ProjectList.tsx | 22 -------- 5 files changed, 111 insertions(+), 26 deletions(-) create mode 100644 frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.test.tsx diff --git a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.test.tsx b/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.test.tsx new file mode 100644 index 0000000000..41f641cce5 --- /dev/null +++ b/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.test.tsx @@ -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( {}} />, { + 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( {}} />, { + permissions: [{ permission: CREATE_PROJECT }], + }); + + await screen.findByText('You have reached the limit for projects'); + + const button = await screen.findByText('Create project'); + expect(button).toBeDisabled(); +}); diff --git a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.tsx b/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.tsx index 87c86c4056..8a8eb48ebe 100644 --- a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.tsx +++ b/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.tsx @@ -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 ( + } + /> + } > diff --git a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/NewProjectForm.tsx b/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/NewProjectForm.tsx index 74d0ab9257..f022b4eac4 100644 --- a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/NewProjectForm.tsx +++ b/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/NewProjectForm.tsx @@ -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 = ({ children, + Limit, handleSubmit, projectName, projectDesc, @@ -324,6 +333,7 @@ export const NewProjectForm: React.FC = ({ } /> + {Limit} {children} ); diff --git a/frontend/src/component/project/ProjectList/ProjectList.test.tsx b/frontend/src/component/project/ProjectList/ProjectList.test.tsx index b01d8fab4b..b33a9c1d74 100644 --- a/frontend/src/component/project/ProjectList/ProjectList.test.tsx +++ b/frontend/src/component/project/ProjectList/ProjectList.test.tsx @@ -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(, { permissions: [{ permission: CREATE_PROJECT }], diff --git a/frontend/src/component/project/ProjectList/ProjectList.tsx b/frontend/src/component/project/ProjectList/ProjectList.tsx index e626936c94..1074286d5f 100644 --- a/frontend/src/component/project/ProjectList/ProjectList.tsx +++ b/frontend/src/component/project/ProjectList/ProjectList.tsx @@ -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 (