Skip to content

Commit

Permalink
feat: Project limit UI (#7518)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Jul 2, 2024
1 parent addbf79 commit e9b6437
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 33 deletions.
40 changes: 40 additions & 0 deletions frontend/src/component/project/ProjectList/ProjectList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { render } from 'utils/testRenderer';
import { ProjectListNew } from './ProjectList';
import { screen, waitFor } from '@testing-library/react';
import { testServerRoute, testServerSetup } from 'utils/testServer';
import { CREATE_PROJECT } from '../../providers/AccessProvider/permissions';

const server = testServerSetup();

const setupApi = () => {
testServerRoute(server, '/api/admin/ui-config', {
flags: {
resourceLimits: true,
},
resourceLimits: {
projects: 1,
},
versionInfo: {
current: { enterprise: 'version' },
},
});

testServerRoute(server, '/api/admin/projects', {
projects: [],
});
};

test('Enabled new project button when limits, version and permission allow for it', async () => {
setupApi();
render(<ProjectListNew />, {
permissions: [{ permission: CREATE_PROJECT }],
});

const button = await screen.findByText('New project');
expect(button).toBeDisabled();

await waitFor(async () => {
const button = await screen.findByText('New project');
expect(button).not.toBeDisabled();
});
});
36 changes: 31 additions & 5 deletions frontend/src/component/project/ProjectList/ProjectList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useContext, useEffect, useMemo, useState } from 'react';
import { type FC, useContext, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
Expand All @@ -24,6 +24,7 @@ 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',
Expand Down Expand Up @@ -53,6 +54,7 @@ const NAVIGATE_TO_CREATE_PROJECT = 'NAVIGATE_TO_CREATE_PROJECT';
function resolveCreateButtonData(
isOss: boolean,
hasAccess: boolean,
limitReached: boolean,
): ICreateButtonData {
if (isOss) {
return {
Expand All @@ -78,6 +80,13 @@ 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' },
Expand All @@ -86,16 +95,31 @@ function resolveCreateButtonData(
}
}

const ProjectCreationButton = () => {
const useProjectLimit = (projectsLimit: number, projectCount: number) => {
const resourceLimitsEnabled = useUiFlag('resourceLimits');
const limitReached = resourceLimitsEnabled && projectCount >= projectsLimit;

return limitReached;
};

const ProjectCreationButton: FC<{ projectCount: number }> = ({
projectCount,
}) => {
const [searchParams] = useSearchParams();
const showCreateDialog = Boolean(searchParams.get('create'));
const [openCreateDialog, setOpenCreateDialog] = useState(showCreateDialog);
const { hasAccess } = useContext(AccessContext);
const { isOss } = useUiConfig();
const { isOss, uiConfig, loading } = useUiConfig();

const limitReached = useProjectLimit(
uiConfig.resourceLimits.projects,
projectCount,
);

const createButtonData = resolveCreateButtonData(
isOss(),
hasAccess(CREATE_PROJECT),
limitReached,
);

return (
Expand All @@ -106,7 +130,7 @@ const ProjectCreationButton = () => {
onClick={() => setOpenCreateDialog(true)}
maxWidth='700px'
permission={CREATE_PROJECT}
disabled={createButtonData.disabled}
disabled={createButtonData.disabled || loading}
tooltipProps={createButtonData.tooltip}
data-testid={NAVIGATE_TO_CREATE_PROJECT}
>
Expand Down Expand Up @@ -201,7 +225,9 @@ export const ProjectListNew = () => {
</>
}
/>
<ProjectCreationButton />
<ProjectCreationButton
projectCount={projects.length}
/>
</>
}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,6 @@ export const defaultValue: IUiConfig = {
featureEnvironmentStrategies: 30,
environments: 50,
constraintValues: 250,
projects: 500,
},
};
2 changes: 2 additions & 0 deletions frontend/src/openapi/models/resourceLimitsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ export interface ResourceLimitsSchema {
environments: number;
/** The maximum number of values for a single constraint. */
constraintValues: number;
/** The maximum number of projects allowed. */
projects: number;
}
23 changes: 16 additions & 7 deletions src/lib/create-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,23 +649,32 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
process.env.UNLEASH_SIGNAL_TOKENS_PER_ENDPOINT_LIMIT,
5,
),
featureEnvironmentStrategies: parseEnvVarNumber(
process.env.UNLEASH_FEATURE_ENVIRONMENT_STRATEGIES_LIMIT,
30,
featureEnvironmentStrategies: Math.max(
1,
parseEnvVarNumber(
process.env.UNLEASH_FEATURE_ENVIRONMENT_STRATEGIES_LIMIT,
30,
),
),
constraintValues: parseEnvVarNumber(
process.env.UNLEASH_CONSTRAINT_VALUES_LIMIT,
options?.resourceLimits?.constraintValues || 250,
constraintValues: Math.max(
1,
parseEnvVarNumber(
process.env.UNLEASH_CONSTRAINT_VALUES_LIMIT,
options?.resourceLimits?.constraintValues || 250,
),
),
environments: parseEnvVarNumber(
process.env.UNLEASH_ENVIRONMENTS_LIMIT,
50,
),
projects: Math.max(
1,
parseEnvVarNumber(process.env.UNLEASH_PROJECTS_LIMIT, 500),
),
apiTokens: Math.max(
0,
parseEnvVarNumber(process.env.UNLEASH_API_TOKENS_LIMIT, 2000),
),
projects: parseEnvVarNumber(process.env.UNLEASH_PROJECTS_LIMIT, 500),
};

return {
Expand Down
21 changes: 0 additions & 21 deletions src/lib/features/project/project-service.limit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,3 @@ test('Should not allow to exceed project limit', async () => {
"Failed to create project. You can't create more than the established limit of 1.",
);
});

test('Should enforce minimum project limit of 1', async () => {
const INVALID_LIMIT = 0;
const projectService = createFakeProjectService({
getLogger,
flagResolver: alwaysOnFlagResolver,
resourceLimits: {
projects: INVALID_LIMIT,
},
} as unknown as IUnleashConfig);

const createProject = (name: string) =>
projectService.createProject({ name }, {} as IUser, {} as IAuditUser);

// allow to create one project
await createProject('projectA');

await expect(() => createProject('projectB')).rejects.toThrow(
"Failed to create project. You can't create more than the established limit of 1.",
);
});

0 comments on commit e9b6437

Please sign in to comment.