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