mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-18 01:18:23 +02:00
feat: Project limit UI (#7518)
This commit is contained in:
parent
addbf79d95
commit
e9b643761c
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -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 { useSearchParams } from 'react-router-dom';
|
||||||
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
@ -24,6 +24,7 @@ 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',
|
||||||
@ -53,6 +54,7 @@ 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 {
|
||||||
@ -78,6 +80,13 @@ 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' },
|
||||||
@ -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 [searchParams] = useSearchParams();
|
||||||
const showCreateDialog = Boolean(searchParams.get('create'));
|
const showCreateDialog = Boolean(searchParams.get('create'));
|
||||||
const [openCreateDialog, setOpenCreateDialog] = useState(showCreateDialog);
|
const [openCreateDialog, setOpenCreateDialog] = useState(showCreateDialog);
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const { isOss } = 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 (
|
||||||
@ -106,7 +130,7 @@ const ProjectCreationButton = () => {
|
|||||||
onClick={() => setOpenCreateDialog(true)}
|
onClick={() => setOpenCreateDialog(true)}
|
||||||
maxWidth='700px'
|
maxWidth='700px'
|
||||||
permission={CREATE_PROJECT}
|
permission={CREATE_PROJECT}
|
||||||
disabled={createButtonData.disabled}
|
disabled={createButtonData.disabled || loading}
|
||||||
tooltipProps={createButtonData.tooltip}
|
tooltipProps={createButtonData.tooltip}
|
||||||
data-testid={NAVIGATE_TO_CREATE_PROJECT}
|
data-testid={NAVIGATE_TO_CREATE_PROJECT}
|
||||||
>
|
>
|
||||||
@ -201,7 +225,9 @@ export const ProjectListNew = () => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ProjectCreationButton />
|
<ProjectCreationButton
|
||||||
|
projectCount={projects.length}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -40,5 +40,6 @@ export const defaultValue: IUiConfig = {
|
|||||||
featureEnvironmentStrategies: 30,
|
featureEnvironmentStrategies: 30,
|
||||||
environments: 50,
|
environments: 50,
|
||||||
constraintValues: 250,
|
constraintValues: 250,
|
||||||
|
projects: 500,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -30,4 +30,6 @@ export interface ResourceLimitsSchema {
|
|||||||
environments: number;
|
environments: number;
|
||||||
/** The maximum number of values for a single constraint. */
|
/** The maximum number of values for a single constraint. */
|
||||||
constraintValues: number;
|
constraintValues: number;
|
||||||
|
/** The maximum number of projects allowed. */
|
||||||
|
projects: number;
|
||||||
}
|
}
|
||||||
|
@ -649,23 +649,32 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
|||||||
process.env.UNLEASH_SIGNAL_TOKENS_PER_ENDPOINT_LIMIT,
|
process.env.UNLEASH_SIGNAL_TOKENS_PER_ENDPOINT_LIMIT,
|
||||||
5,
|
5,
|
||||||
),
|
),
|
||||||
featureEnvironmentStrategies: parseEnvVarNumber(
|
featureEnvironmentStrategies: Math.max(
|
||||||
|
1,
|
||||||
|
parseEnvVarNumber(
|
||||||
process.env.UNLEASH_FEATURE_ENVIRONMENT_STRATEGIES_LIMIT,
|
process.env.UNLEASH_FEATURE_ENVIRONMENT_STRATEGIES_LIMIT,
|
||||||
30,
|
30,
|
||||||
),
|
),
|
||||||
constraintValues: parseEnvVarNumber(
|
),
|
||||||
|
constraintValues: Math.max(
|
||||||
|
1,
|
||||||
|
parseEnvVarNumber(
|
||||||
process.env.UNLEASH_CONSTRAINT_VALUES_LIMIT,
|
process.env.UNLEASH_CONSTRAINT_VALUES_LIMIT,
|
||||||
options?.resourceLimits?.constraintValues || 250,
|
options?.resourceLimits?.constraintValues || 250,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
environments: parseEnvVarNumber(
|
environments: parseEnvVarNumber(
|
||||||
process.env.UNLEASH_ENVIRONMENTS_LIMIT,
|
process.env.UNLEASH_ENVIRONMENTS_LIMIT,
|
||||||
50,
|
50,
|
||||||
),
|
),
|
||||||
|
projects: Math.max(
|
||||||
|
1,
|
||||||
|
parseEnvVarNumber(process.env.UNLEASH_PROJECTS_LIMIT, 500),
|
||||||
|
),
|
||||||
apiTokens: Math.max(
|
apiTokens: Math.max(
|
||||||
0,
|
0,
|
||||||
parseEnvVarNumber(process.env.UNLEASH_API_TOKENS_LIMIT, 2000),
|
parseEnvVarNumber(process.env.UNLEASH_API_TOKENS_LIMIT, 2000),
|
||||||
),
|
),
|
||||||
projects: parseEnvVarNumber(process.env.UNLEASH_PROJECTS_LIMIT, 500),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -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.",
|
"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.",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
Loading…
Reference in New Issue
Block a user