diff --git a/frontend/src/component/changeRequest/ChangeRequest.test.tsx b/frontend/src/component/changeRequest/ChangeRequest.test.tsx
index 736115063b..7c3a2230c7 100644
--- a/frontend/src/component/changeRequest/ChangeRequest.test.tsx
+++ b/frontend/src/component/changeRequest/ChangeRequest.test.tsx
@@ -86,6 +86,9 @@ const uiConfigForEnterprise = () =>
flags: {
changeRequests: true,
},
+ resourceLimits: {
+ featureEnvironmentStrategies: 10,
+ },
slogan: 'getunleash.io - All rights reserved',
name: 'Unleash enterprise',
links: [
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.test.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.test.tsx
index 009db24cdc..e3f39815fb 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.test.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.test.tsx
@@ -8,11 +8,16 @@ import type { IFeatureStrategy } from 'interfaces/strategy';
const server = testServerSetup();
+const LIMIT = 3;
+
const setupApi = () => {
testServerRoute(server, '/api/admin/ui-config', {
flags: {
resourceLimits: true,
},
+ resourceLimits: {
+ featureEnvironmentStrategies: LIMIT,
+ },
});
testServerRoute(
@@ -45,7 +50,7 @@ const environmentWithManyStrategies = {
name: 'production',
enabled: true,
type: 'production',
- strategies: [...Array(30).keys()].map(() => strategy),
+ strategies: [...Array(LIMIT).keys()].map(() => strategy),
};
test('should allow to add strategy when no strategies', async () => {
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx
index 4dada03ff1..a14391e41e 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx
@@ -23,6 +23,7 @@ import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureS
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
import { Badge } from 'component/common/Badge/Badge';
import { useUiFlag } from 'hooks/useUiFlag';
+import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
interface IFeatureOverviewEnvironmentProps {
env: IFeatureEnvironment;
@@ -116,6 +117,19 @@ const StyledButtonContainer = styled('div')(({ theme }) => ({
},
}));
+const useStrategyLimit = (strategyCount: number) => {
+ const resourceLimitsEnabled = useUiFlag('resourceLimits');
+ const { uiConfig } = useUiConfig();
+ const featureEnvironmentStrategiesLimit =
+ uiConfig.resourceLimits?.featureEnvironmentStrategies || 100;
+ const limitReached =
+ resourceLimitsEnabled &&
+ strategyCount >= featureEnvironmentStrategiesLimit;
+ const limitMessage = `Limit of ${featureEnvironmentStrategiesLimit} strategies reached`;
+
+ return { limitReached, limitMessage };
+};
+
const FeatureOverviewEnvironment = ({
env,
}: IFeatureOverviewEnvironmentProps) => {
@@ -132,11 +146,10 @@ 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;
+
+ const { limitMessage, limitReached } = useStrategyLimit(
+ featureEnvironment?.strategies.length || 0,
+ );
return (
@@ -234,7 +247,7 @@ const FeatureOverviewEnvironment = ({
environmentId={env.name}
disableReason={
limitReached
- ? 'Limit reached'
+ ? limitMessage
: undefined
}
/>
diff --git a/frontend/src/hooks/api/getters/useUiConfig/defaultValue.tsx b/frontend/src/hooks/api/getters/useUiConfig/defaultValue.tsx
index 51902037e4..22216af732 100644
--- a/frontend/src/hooks/api/getters/useUiConfig/defaultValue.tsx
+++ b/frontend/src/hooks/api/getters/useUiConfig/defaultValue.tsx
@@ -28,4 +28,15 @@ export const defaultValue: IUiConfig = {
},
],
networkViewEnabled: false,
+ resourceLimits: {
+ segmentValues: 1000,
+ strategySegments: 5,
+ signalEndpoints: 5,
+ actionSetActions: 10,
+ actionSetsPerProject: 5,
+ actionSetFilters: 5,
+ actionSetFilterValues: 25,
+ signalTokensPerEndpoint: 5,
+ featureEnvironmentStrategies: 30,
+ },
};
diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts
index 6a565db72a..f3154381bf 100644
--- a/frontend/src/interfaces/uiConfig.ts
+++ b/frontend/src/interfaces/uiConfig.ts
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import type { Variant } from 'utils/variants';
+import type { ResourceLimitsSchema } from '../openapi';
export interface IUiConfig {
authenticationType?: string;
@@ -28,6 +29,7 @@ export interface IUiConfig {
segmentValuesLimit?: number;
strategySegmentsLimit?: number;
frontendApiOrigins?: string[];
+ resourceLimits: ResourceLimitsSchema;
}
export interface IProclamationToast {
diff --git a/frontend/src/openapi/models/resourceLimitsSchema.ts b/frontend/src/openapi/models/resourceLimitsSchema.ts
index 38ea977a12..d709a472dd 100644
--- a/frontend/src/openapi/models/resourceLimitsSchema.ts
+++ b/frontend/src/openapi/models/resourceLimitsSchema.ts
@@ -24,4 +24,6 @@ export interface ResourceLimitsSchema {
signalTokensPerEndpoint: number;
/** The maximum number of strategy segments allowed. */
strategySegments: number;
+ /** The maximum number of feature environment strategies allowed. */
+ featureEnvironmentStrategies: number;
}
diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap
index f0879a37a5..ed50e35aa9 100644
--- a/src/lib/__snapshots__/create-config.test.ts.snap
+++ b/src/lib/__snapshots__/create-config.test.ts.snap
@@ -196,6 +196,7 @@ exports[`should create default config 1`] = `
"actionSetFilterValues": 25,
"actionSetFilters": 5,
"actionSetsPerProject": 5,
+ "featureEnvironmentStrategies": 30,
"segmentValues": 1000,
"signalEndpoints": 5,
"signalTokensPerEndpoint": 5,
diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts
index 2929f048ef..c589442b33 100644
--- a/src/lib/create-config.ts
+++ b/src/lib/create-config.ts
@@ -649,6 +649,10 @@ 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,
+ ),
};
return {
diff --git a/src/lib/features/feature-toggle/createFeatureToggleService.ts b/src/lib/features/feature-toggle/createFeatureToggleService.ts
index 414d369859..874a088f9b 100644
--- a/src/lib/features/feature-toggle/createFeatureToggleService.ts
+++ b/src/lib/features/feature-toggle/createFeatureToggleService.ts
@@ -60,7 +60,7 @@ export const createFeatureToggleService = (
db: Db,
config: IUnleashConfig,
): FeatureToggleService => {
- const { getLogger, eventBus, flagResolver } = config;
+ const { getLogger, eventBus, flagResolver, resourceLimits } = config;
const featureStrategiesStore = new FeatureStrategiesStore(
db,
eventBus,
@@ -142,7 +142,7 @@ export const createFeatureToggleService = (
contextFieldStore,
strategyStore,
},
- { getLogger, flagResolver, eventBus },
+ { getLogger, flagResolver, eventBus, resourceLimits },
segmentService,
accessService,
eventService,
@@ -156,7 +156,7 @@ export const createFeatureToggleService = (
};
export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
- const { getLogger, flagResolver } = config;
+ const { getLogger, flagResolver, resourceLimits } = config;
const eventStore = new FakeEventStore();
const strategyStore = new FakeStrategiesStore();
const featureStrategiesStore = new FakeFeatureStrategiesStore();
@@ -204,7 +204,12 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
contextFieldStore,
strategyStore,
},
- { getLogger, flagResolver, eventBus: new EventEmitter() },
+ {
+ getLogger,
+ flagResolver,
+ eventBus: new EventEmitter(),
+ resourceLimits,
+ },
segmentService,
accessService,
eventService,
diff --git a/src/lib/features/feature-toggle/feature-toggle-service.ts b/src/lib/features/feature-toggle/feature-toggle-service.ts
index 473371a73a..9ccfe90105 100644
--- a/src/lib/features/feature-toggle/feature-toggle-service.ts
+++ b/src/lib/features/feature-toggle/feature-toggle-service.ts
@@ -108,6 +108,7 @@ import { FEATURES_CREATED_BY_PROCESSED } from '../../metric-events';
import { allSettledWithRejection } from '../../util/allSettledWithRejection';
import type EventEmitter from 'node:events';
import type { IFeatureLifecycleReadModel } from '../feature-lifecycle/feature-lifecycle-read-model-type';
+import type { ResourceLimitsSchema } from '../../openapi';
interface IFeatureContext {
featureName: string;
@@ -179,6 +180,8 @@ class FeatureToggleService {
private eventBus: EventEmitter;
+ private resourceLimits: ResourceLimitsSchema;
+
constructor(
{
featureStrategiesStore,
@@ -204,7 +207,11 @@ class FeatureToggleService {
getLogger,
flagResolver,
eventBus,
- }: Pick,
+ resourceLimits,
+ }: Pick<
+ IUnleashConfig,
+ 'getLogger' | 'flagResolver' | 'eventBus' | 'resourceLimits'
+ >,
segmentService: ISegmentService,
accessService: AccessService,
eventService: EventService,
@@ -233,6 +240,7 @@ class FeatureToggleService {
this.dependentFeaturesService = dependentFeaturesService;
this.featureLifecycleReadModel = featureLifecycleReadModel;
this.eventBus = eventBus;
+ this.resourceLimits = resourceLimits;
}
async validateFeaturesContext(
@@ -647,7 +655,7 @@ class FeatureToggleService {
await this.validateStrategyLimit(
{ featureName, projectId, environment },
- 30,
+ this.resourceLimits.featureEnvironmentStrategies,
);
try {
diff --git a/src/lib/features/feature-toggle/tests/feature-toggle-service.limit.test.ts b/src/lib/features/feature-toggle/tests/feature-toggle-service.limit.test.ts
index 7013a29681..d0ade8abd3 100644
--- a/src/lib/features/feature-toggle/tests/feature-toggle-service.limit.test.ts
+++ b/src/lib/features/feature-toggle/tests/feature-toggle-service.limit.test.ts
@@ -14,10 +14,14 @@ const alwaysOnFlagResolver = {
} as unknown as IFlagResolver;
test('Should not allow to exceed strategy limit', async () => {
+ const LIMIT = 3;
const { featureToggleService, featureToggleStore } =
createFakeFeatureToggleService({
getLogger,
flagResolver: alwaysOnFlagResolver,
+ resourceLimits: {
+ featureEnvironmentStrategies: LIMIT,
+ },
} as unknown as IUnleashConfig);
const addStrategy = () =>
@@ -31,11 +35,11 @@ test('Should not allow to exceed strategy limit', async () => {
createdByUserId: 1,
});
- for (let i = 0; i < 30; i++) {
+ for (let i = 0; i < LIMIT; i++) {
await addStrategy();
}
await expect(addStrategy()).rejects.toThrow(
- 'Strategy limit of 30 exceeded',
+ `Strategy limit of ${LIMIT} exceeded`,
);
});
diff --git a/src/lib/openapi/spec/resource-limits-schema.ts b/src/lib/openapi/spec/resource-limits-schema.ts
index ea295ed996..cfdc90a406 100644
--- a/src/lib/openapi/spec/resource-limits-schema.ts
+++ b/src/lib/openapi/spec/resource-limits-schema.ts
@@ -13,6 +13,7 @@ export const resourceLimitsSchema = {
'actionSetFilterValues',
'signalEndpoints',
'signalTokensPerEndpoint',
+ 'featureEnvironmentStrategies',
],
additionalProperties: false,
properties: {
@@ -61,6 +62,12 @@ export const resourceLimitsSchema = {
description:
'The maximum number of signal tokens per endpoint allowed.',
},
+ featureEnvironmentStrategies: {
+ type: 'integer',
+ example: 30,
+ description:
+ 'The maximum number of feature environment strategies allowed.',
+ },
},
components: {},
} as const;