1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: strategy limit to 30 (#7473)

This commit is contained in:
Mateusz Kwasniewski 2024-06-28 11:18:44 +02:00 committed by GitHub
parent fbda7cdc48
commit 5bd32f264d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 194 additions and 8 deletions

View File

@ -19,6 +19,7 @@ interface IFeatureStrategyMenuProps {
variant?: IPermissionButtonProps['variant'];
matchWidth?: boolean;
size?: IPermissionButtonProps['size'];
disableReason?: string;
}
const StyledStrategyMenu = styled('div')({
@ -43,6 +44,7 @@ export const FeatureStrategyMenu = ({
variant,
size,
matchWidth,
disableReason,
}: IFeatureStrategyMenuProps) => {
const [anchor, setAnchor] = useState<Element>();
const navigate = useNavigate();
@ -86,6 +88,10 @@ export const FeatureStrategyMenu = ({
variant={variant}
size={size}
sx={{ minWidth: matchWidth ? '282px' : 'auto' }}
disabled={Boolean(disableReason)}
tooltipProps={{
title: disableReason ? disableReason : undefined,
}}
>
{label}
</PermissionButton>
@ -99,8 +105,9 @@ export const FeatureStrategyMenu = ({
variant='outlined'
size={size}
hideLockIcon
disabled={Boolean(disableReason)}
tooltipProps={{
title: 'More strategies',
title: disableReason ? disableReason : 'More strategies',
}}
>
<MoreVert

View File

@ -0,0 +1,97 @@
import { screen, waitFor } from '@testing-library/react';
import { render } from 'utils/testRenderer';
import FeatureOverviewEnvironment from './FeatureOverviewEnvironment';
import { testServerRoute, testServerSetup } from 'utils/testServer';
import { Route, Routes } from 'react-router-dom';
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
import type { IFeatureStrategy } from 'interfaces/strategy';
const server = testServerSetup();
const setupApi = () => {
testServerRoute(server, '/api/admin/ui-config', {
flags: {
resourceLimits: true,
},
});
testServerRoute(
server,
'/api/admin/projects/default/features/featureWithoutStrategies',
{
environments: [environmentWithoutStrategies],
},
);
testServerRoute(
server,
'/api/admin/projects/default/features/featureWithManyStrategies',
{
environments: [environmentWithManyStrategies],
},
);
};
const strategy = {
name: 'default',
} as IFeatureStrategy;
const environmentWithoutStrategies = {
name: 'production',
enabled: true,
type: 'production',
strategies: [],
};
const environmentWithManyStrategies = {
name: 'production',
enabled: true,
type: 'production',
strategies: [...Array(30).keys()].map(() => strategy),
};
test('should allow to add strategy when no strategies', async () => {
setupApi();
render(
<Routes>
<Route
path='/projects/:projectId/features/:featureId/strategies/create'
element={
<FeatureOverviewEnvironment
env={environmentWithoutStrategies}
/>
}
/>
</Routes>,
{
route: '/projects/default/features/featureWithoutStrategies/strategies/create',
permissions: [{ permission: CREATE_FEATURE_STRATEGY }],
},
);
const button = await screen.findByText('Add strategy');
expect(button).toBeEnabled();
});
test('should not allow to add strategy when limit reached', async () => {
setupApi();
render(
<Routes>
<Route
path='/projects/:projectId/features/:featureId/strategies/create'
element={
<FeatureOverviewEnvironment
env={environmentWithManyStrategies}
/>
}
/>
</Routes>,
{
route: '/projects/default/features/featureWithManyStrategies/strategies/create',
permissions: [{ permission: CREATE_FEATURE_STRATEGY }],
},
);
await waitFor(async () => {
const button = await screen.findByText('Add strategy');
expect(button).toBeDisabled();
});
});

View File

@ -22,6 +22,7 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureStrategyIcons/FeatureStrategyIcons';
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
import { Badge } from 'component/common/Badge/Badge';
import { useUiFlag } from 'hooks/useUiFlag';
interface IFeatureOverviewEnvironmentProps {
env: IFeatureEnvironment;
@ -131,6 +132,11 @@ const FeatureOverviewEnvironment = ({
const featureEnvironment = feature?.environments.find(
(featureEnvironment) => featureEnvironment.name === env.name,
);
const resourceLimitsEnabled = useUiFlag('resourceLimits');
const limitReached =
resourceLimitsEnabled &&
Array.isArray(featureEnvironment?.strategies) &&
featureEnvironment?.strategies.length >= 30;
return (
<ConditionallyRender
@ -179,6 +185,11 @@ const FeatureOverviewEnvironment = ({
environmentId={env.name}
variant='outlined'
size='small'
disableReason={
limitReached
? 'Limit reached'
: undefined
}
/>
<FeatureStrategyIcons
strategies={
@ -221,6 +232,11 @@ const FeatureOverviewEnvironment = ({
projectId={projectId}
featureId={featureId}
environmentId={env.name}
disableReason={
limitReached
? 'Limit reached'
: undefined
}
/>
</Box>
<EnvironmentFooter

View File

@ -68,7 +68,7 @@ export const createFakeExportImportTogglesService = (
const featureStrategiesStore = new FakeFeatureStrategiesStore();
const featureEnvironmentStore = new FakeFeatureEnvironmentStore();
const { accessService } = createFakeAccessService(config);
const featureToggleService = createFakeFeatureToggleService(config);
const { featureToggleService } = createFakeFeatureToggleService(config);
const privateProjectChecker = createFakePrivateProjectChecker();
const eventService = new EventService(

View File

@ -155,9 +155,7 @@ export const createFeatureToggleService = (
return featureToggleService;
};
export const createFakeFeatureToggleService = (
config: IUnleashConfig,
): FeatureToggleService => {
export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
const { getLogger, flagResolver } = config;
const eventStore = new FakeEventStore();
const strategyStore = new FakeStrategiesStore();
@ -216,5 +214,5 @@ export const createFakeFeatureToggleService = (
dependentFeaturesService,
featureLifecycleReadModel,
);
return featureToggleService;
return { featureToggleService, featureToggleStore };
};

View File

@ -358,6 +358,27 @@ class FeatureToggleService {
}
}
async validateStrategyLimit(
featureEnv: {
projectId: string;
environment: string;
featureName: string;
},
limit: number,
) {
if (!this.flagResolver.isEnabled('resourceLimits')) return;
const existingCount = (
await this.featureStrategiesStore.getStrategiesForFeatureEnv(
featureEnv.projectId,
featureEnv.featureName,
featureEnv.environment,
)
).length;
if (existingCount >= limit) {
throw new BadDataError(`Strategy limit of ${limit} exceeded}.`);
}
}
async validateStrategyType(
strategyName: string | undefined,
): Promise<void> {
@ -624,6 +645,11 @@ class FeatureToggleService {
strategyConfig.variants = fixedVariants;
}
await this.validateStrategyLimit(
{ featureName, projectId, environment },
30,
);
try {
const newFeatureStrategy =
await this.featureStrategiesStore.createStrategyFeatureEnv({

View File

@ -0,0 +1,41 @@
import { createFakeFeatureToggleService } from '../createFeatureToggleService';
import type {
IAuditUser,
IFlagResolver,
IStrategyConfig,
IUnleashConfig,
} from '../../../types';
import getLogger from '../../../../test/fixtures/no-logger';
const alwaysOnFlagResolver = {
isEnabled() {
return true;
},
} as unknown as IFlagResolver;
test('Should not allow to exceed strategy limit', async () => {
const { featureToggleService, featureToggleStore } =
createFakeFeatureToggleService({
getLogger,
flagResolver: alwaysOnFlagResolver,
} as unknown as IUnleashConfig);
const addStrategy = () =>
featureToggleService.unprotectedCreateStrategy(
{ name: 'default', featureName: 'feature' } as IStrategyConfig,
{ projectId: 'default', featureName: 'feature' } as any,
{} as IAuditUser,
);
await featureToggleStore.create('default', {
name: 'feature',
createdByUserId: 1,
});
for (let i = 0; i < 30; i++) {
await addStrategy();
}
await expect(addStrategy()).rejects.toThrow(
'Strategy limit of 30 exceeded',
);
});

View File

@ -72,7 +72,8 @@ export const createFakeFrontendApiService = (
eventService,
);
// TODO: remove this dependency after we migrate frontend API
const featureToggleServiceV2 = createFakeFeatureToggleService(config);
const featureToggleServiceV2 =
createFakeFeatureToggleService(config).featureToggleService;
const clientFeatureToggleReadModel = new FakeClientFeatureToggleReadModel();
const globalFrontendApiCache = new GlobalFrontendApiCache(
config,

View File

@ -150,7 +150,7 @@ export const createFakeProjectService = (
const featureTypeStore = new FakeFeatureTypeStore();
const projectStatsStore = new FakeProjectStatsStore();
const { accessService } = createFakeAccessService(config);
const featureToggleService = createFakeFeatureToggleService(config);
const { featureToggleService } = createFakeFeatureToggleService(config);
const favoriteFeaturesStore = new FakeFavoriteFeaturesStore();
const favoriteProjectsStore = new FakeFavoriteProjectsStore();
const eventService = new EventService(