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:
parent
fbda7cdc48
commit
5bd32f264d
@ -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
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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 };
|
||||
};
|
||||
|
@ -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({
|
||||
|
@ -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',
|
||||
);
|
||||
});
|
@ -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,
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user