diff --git a/frontend/src/component/common/Limit/Limit.tsx b/frontend/src/component/common/Limit/Limit.tsx
index 5075af6393..75e1476d42 100644
--- a/frontend/src/component/common/Limit/Limit.tsx
+++ b/frontend/src/component/common/Limit/Limit.tsx
@@ -82,7 +82,7 @@ export const Limit: FC<{
const footerContent = isOss() ? (
<>
- Need help with resource limits? Try the the{' '}
+ Need help with resource limits? Try the{' '}
Unleash community Slack.
>
) : (
diff --git a/frontend/src/component/releases/ReleasePlanTemplate/CreateReleasePlanTemplate.tsx b/frontend/src/component/releases/ReleasePlanTemplate/CreateReleasePlanTemplate.tsx
index d3ae6a648b..706279666d 100644
--- a/frontend/src/component/releases/ReleasePlanTemplate/CreateReleasePlanTemplate.tsx
+++ b/frontend/src/component/releases/ReleasePlanTemplate/CreateReleasePlanTemplate.tsx
@@ -13,6 +13,8 @@ import { formatUnknownError } from 'utils/formatUnknownError';
import { useUiFlag } from 'hooks/useUiFlag';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
+import { Limit } from 'component/common/Limit/Limit.tsx';
+import { useReleasePlanTemplates } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates.ts';
const StyledButtonContainer = styled('div')(() => ({
marginTop: 'auto',
@@ -31,7 +33,12 @@ export const CreateReleasePlanTemplate = () => {
const navigate = useNavigate();
const { createReleasePlanTemplate } = useReleasePlanTemplatesApi();
const { trackEvent } = usePlausibleTracker();
+ const { templates } = useReleasePlanTemplates();
+ const releaseTemplateLimit = uiConfig.resourceLimits.releaseTemplates;
+ const canCreateMore = templates.length < releaseTemplateLimit;
+
usePageTitle('Create release template');
+
const {
name,
setName,
@@ -102,11 +109,19 @@ export const CreateReleasePlanTemplate = () => {
formTitle='Create release template'
formatApiCode={formatApiCode}
handleSubmit={handleSubmit}
+ Limit={
+
+ }
>
Save template
diff --git a/frontend/src/component/releases/ReleasePlanTemplate/TemplateForm/TemplateForm.tsx b/frontend/src/component/releases/ReleasePlanTemplate/TemplateForm/TemplateForm.tsx
index 26242fb3de..cc674b023d 100644
--- a/frontend/src/component/releases/ReleasePlanTemplate/TemplateForm/TemplateForm.tsx
+++ b/frontend/src/component/releases/ReleasePlanTemplate/TemplateForm/TemplateForm.tsx
@@ -1,5 +1,5 @@
import Input from 'component/common/Input/Input';
-import { Alert, styled, useTheme } from '@mui/material';
+import { Alert, Box, styled, useTheme } from '@mui/material';
import type { IReleasePlanMilestonePayload } from 'interfaces/releasePlans';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { TemplateFormDescription } from './TemplateFormDescription.tsx';
@@ -33,6 +33,14 @@ const StyledForm = styled('form')(({ theme }) => ({
paddingTop: theme.spacing(5),
}));
+const StyledLimitContainer = styled(Box)(({ theme }) => ({
+ flex: 1,
+ display: 'flex',
+ alignItems: 'flex-end',
+ marginTop: theme.spacing(3),
+ marginBottom: theme.spacing(3),
+}));
+
interface ITemplateFormProps {
name: string;
setName: React.Dispatch>;
@@ -49,6 +57,7 @@ interface ITemplateFormProps {
formatApiCode: () => string;
handleSubmit: (e: React.FormEvent) => void;
loading?: boolean;
+ Limit?: React.ReactNode;
children?: React.ReactNode;
}
@@ -65,6 +74,7 @@ export const TemplateForm: React.FC = ({
archived,
formatApiCode,
handleSubmit,
+ Limit,
children,
}) => {
const theme = useTheme();
@@ -135,6 +145,8 @@ export const TemplateForm: React.FC = ({
milestoneChanged={milestoneChanged}
/>
+ {Limit}
+
{children}
diff --git a/frontend/src/hooks/api/getters/useUiConfig/defaultValue.tsx b/frontend/src/hooks/api/getters/useUiConfig/defaultValue.tsx
index 4468420767..c55fa2b692 100644
--- a/frontend/src/hooks/api/getters/useUiConfig/defaultValue.tsx
+++ b/frontend/src/hooks/api/getters/useUiConfig/defaultValue.tsx
@@ -45,5 +45,6 @@ export const defaultValue: IUiConfig = {
segments: 300,
apiTokens: 2000,
featureFlags: 5000,
+ releaseTemplates: 5,
},
};
diff --git a/frontend/src/openapi/models/resourceLimitsSchema.ts b/frontend/src/openapi/models/resourceLimitsSchema.ts
index b4afa0473b..2e7802a9fa 100644
--- a/frontend/src/openapi/models/resourceLimitsSchema.ts
+++ b/frontend/src/openapi/models/resourceLimitsSchema.ts
@@ -42,6 +42,8 @@ export interface ResourceLimitsSchema {
* @minimum 1
*/
projects: number;
+ /** The maximum number of release templates allowed. */
+ releaseTemplates: number;
/** The maximum number of segments allowed. */
segments: number;
/** The maximum number of values per segment allowed. */
diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap
index 455feba0d7..fe6bece2cb 100644
--- a/src/lib/__snapshots__/create-config.test.ts.snap
+++ b/src/lib/__snapshots__/create-config.test.ts.snap
@@ -121,6 +121,7 @@ exports[`should create default config 1`] = `
"featureEnvironmentStrategies": 30,
"featureFlags": 5000,
"projects": 500,
+ "releaseTemplates": 5,
"segmentValues": 1000,
"segments": 300,
"signalEndpoints": 5,
diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts
index b631cf7456..2f775266b2 100644
--- a/src/lib/create-config.ts
+++ b/src/lib/create-config.ts
@@ -758,6 +758,13 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
options?.resourceLimits?.featureFlags ?? 5000,
),
),
+ releaseTemplates: Math.max(
+ 0,
+ parseEnvVarNumber(
+ process.env.UNLEASH_RELEASE_TEMPLATES_LIMIT,
+ options?.resourceLimits?.releaseTemplates ?? 5,
+ ),
+ ),
};
const openAIAPIKey = process.env.OPENAI_API_KEY;
diff --git a/src/lib/openapi/spec/resource-limits-schema.ts b/src/lib/openapi/spec/resource-limits-schema.ts
index 0b8445caa7..00ee053b32 100644
--- a/src/lib/openapi/spec/resource-limits-schema.ts
+++ b/src/lib/openapi/spec/resource-limits-schema.ts
@@ -21,6 +21,7 @@ export const resourceLimitsSchema = {
'segments',
'featureFlags',
'constraints',
+ 'releaseTemplates',
],
additionalProperties: false,
properties: {
@@ -118,6 +119,11 @@ export const resourceLimitsSchema = {
description:
'The maximum number of feature flags you can have at the same time. Archived flags do not count towards this limit.',
},
+ releaseTemplates: {
+ type: 'integer',
+ example: 5,
+ description: 'The maximum number of release templates allowed.',
+ },
},
components: {},
} as const;
diff --git a/src/lib/openapi/spec/ui-config-schema.test.ts b/src/lib/openapi/spec/ui-config-schema.test.ts
index 4ccd6d7940..9ec245f4ac 100644
--- a/src/lib/openapi/spec/ui-config-schema.test.ts
+++ b/src/lib/openapi/spec/ui-config-schema.test.ts
@@ -26,6 +26,7 @@ test('uiConfigSchema', () => {
segments: 0,
featureFlags: 1,
constraints: 0,
+ releaseTemplates: 0,
},
versionInfo: {
current: {},
diff --git a/src/lib/types/option.ts b/src/lib/types/option.ts
index 6af0a001f9..9a5d7a8ed6 100644
--- a/src/lib/types/option.ts
+++ b/src/lib/types/option.ts
@@ -135,6 +135,7 @@ export interface ResourceLimits {
actionSetFilterValues: number;
signalEndpoints: number;
signalTokensPerEndpoint: number;
+ releaseTemplates: number;
}
export interface IUnleashOptions {