1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +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 { 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>

View File

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

View File

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

View File

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