1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-10 17:53:36 +02:00

feat: project limits ui (#7558)

This commit is contained in:
Mateusz Kwasniewski 2024-07-09 11:04:23 +02:00 committed by GitHub
parent 46b1eedcc7
commit 2aea6e688c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 111 additions and 26 deletions

View File

@ -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();
});

View File

@ -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>

View File

@ -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>
); );

View File

@ -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 }],

View File

@ -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 (