From edae8801d8f294fbae23ab08d81d2ea5afca9b4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Thu, 21 Aug 2025 11:52:09 +0100 Subject: [PATCH] chore: release template resource limits (#10514) https://linear.app/unleash/issue/2-3790/use-resource-limits-to-limit-the-amount-of-release-templates-you-can Adds a new resource limit for release templates. Needs a follow-up PR in Enterprise. --- frontend/src/component/common/Limit/Limit.tsx | 2 +- .../CreateReleasePlanTemplate.tsx | 15 +++++++++++++++ .../TemplateForm/TemplateForm.tsx | 14 +++++++++++++- .../api/getters/useUiConfig/defaultValue.tsx | 1 + .../src/openapi/models/resourceLimitsSchema.ts | 2 ++ src/lib/__snapshots__/create-config.test.ts.snap | 1 + src/lib/create-config.ts | 7 +++++++ src/lib/openapi/spec/resource-limits-schema.ts | 6 ++++++ src/lib/openapi/spec/ui-config-schema.test.ts | 1 + src/lib/types/option.ts | 1 + 10 files changed, 48 insertions(+), 2 deletions(-) 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 {