From e14c32015fefd915f267ce1542bed3ff8b5ba3e2 Mon Sep 17 00:00:00 2001 From: martmull Date: Tue, 21 Jul 2026 16:22:19 +0200 Subject: [PATCH] Make upgrade applications batch size a job parameter defaulting to 5 (#23101) Makes the batch size used when upgrading applications a parameter instead of a hardcoded constant, defaulting to 5, and lets admins set it from the upgrade confirmation modal. Backend: - `UpgradeApplicationsJobData` gains an optional `batchSize` field, passed through by `UpgradeApplicationsJob` to the service. - `ApplicationUpgradeService.upgradeAllApplications` accepts an optional `batchSize` parameter, defaulting to `UPGRADE_APPLICATIONS_DEFAULT_BATCH_SIZE = 5` (previously a fixed batch size of 20). The value is sanitized to a positive integer to avoid an infinite batching loop. - The `upgradeRegistrationApplications` admin mutation accepts an optional `batchSize: Int` argument and forwards it to the job. Frontend (admin panel): - The "Upgrade existing installations" confirmation modal now includes a "Batch size" number input, defaulting to 5, sent with the mutation. - Updated the admin GraphQL document and generated types. ## Screenshots Upgrade section on the admin app registration page: ![Upgrade section](https://raw.githubusercontent.com/twentyhq/twenty/8f6f7b5b87b8218e10f9f8ed8a8b705cf25f22f6/upgrade-section.png) Confirmation modal with the new batch size input (defaults to 5): ![Upgrade confirmation modal with batch size input](https://raw.githubusercontent.com/twentyhq/twenty/8f6f7b5b87b8218e10f9f8ed8a8b705cf25f22f6/upgrade-modal-batch-size.png) --------- Co-authored-by: Martin --- .../src/generated-admin/graphql.ts | 4 +- .../upgradeRegistrationApplications.ts | 2 + ...ngsApplicationRegistrationGeneralStats.tsx | 41 ++++++++++++++++++- .../admin-panel/admin-panel.resolver.ts | 8 +++- .../application-upgrade.service.ts | 10 +++-- .../upgrade-applications.job-constants.ts | 1 + .../jobs/upgrade-applications.job.ts | 1 + 7 files changed, 60 insertions(+), 7 deletions(-) diff --git a/packages/twenty-front/src/generated-admin/graphql.ts b/packages/twenty-front/src/generated-admin/graphql.ts index 465fbf8ea9..5d14057f8f 100644 --- a/packages/twenty-front/src/generated-admin/graphql.ts +++ b/packages/twenty-front/src/generated-admin/graphql.ts @@ -590,6 +590,7 @@ export type MutationUpdateWorkspaceFeatureFlagArgs = { export type MutationUpgradeRegistrationApplicationsArgs = { applicationRegistrationId: Scalars['String']['input']; + batchSize?: InputMaybe; }; export type PaginatedApplicationRegistrations = { @@ -1115,6 +1116,7 @@ export type UpdateAdminApplicationRegistrationVariableMutation = { __typename?: export type UpgradeRegistrationApplicationsMutationVariables = Exact<{ applicationRegistrationId: Scalars['String']['input']; + batchSize?: InputMaybe; }>; @@ -1396,7 +1398,7 @@ export const BackfillApplicationInstallationDocument = {"kind":"Document","defin export const SyncMarketplaceCatalogDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncMarketplaceCatalog"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"syncMarketplaceCatalog"}}]}}]} as unknown as DocumentNode; export const UpdateAdminApplicationRegistrationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateAdminApplicationRegistration"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateApplicationRegistrationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateAdminApplicationRegistration"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ApplicationRegistrationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApplicationRegistrationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApplicationRegistration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"galleryImagesUrls"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthClientId"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthRedirectUris"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthScopes"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isVetted"}},{"kind":"Field","name":{"kind":"Name","value":"isPreInstalled"}},{"kind":"Field","name":{"kind":"Name","value":"isConfigured"}},{"kind":"Field","name":{"kind":"Name","value":"ownerWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const UpdateAdminApplicationRegistrationVariableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateAdminApplicationRegistrationVariable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateApplicationRegistrationVariableInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateAdminApplicationRegistrationVariable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"isSecret"}},{"kind":"Field","name":{"kind":"Name","value":"isRequired"}},{"kind":"Field","name":{"kind":"Name","value":"isFilled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; -export const UpgradeRegistrationApplicationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpgradeRegistrationApplications"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upgradeRegistrationApplications"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}}}]}]}}]} as unknown as DocumentNode; +export const UpgradeRegistrationApplicationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpgradeRegistrationApplications"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"batchSize"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upgradeRegistrationApplications"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"batchSize"},"value":{"kind":"Variable","name":{"kind":"Name","value":"batchSize"}}}]}]}}]} as unknown as DocumentNode; export const FindAdminApplicationRegistrationClaimsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAdminApplicationRegistrationClaims"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAdminApplicationRegistrationClaims"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceDisplayName"}}]}}]}}]} as unknown as DocumentNode; export const FindAdminApplicationRegistrationInstalledWorkspacesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAdminApplicationRegistrationInstalledWorkspaces"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FindApplicationRegistrationInstalledWorkspacesInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAdminApplicationRegistrationInstalledWorkspaces"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"hasMore"}},{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"version"}}]}}]}}]}}]} as unknown as DocumentNode; export const FindAdminApplicationRegistrationStatsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAdminApplicationRegistrationStats"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAdminApplicationRegistrationStats"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeInstalls"}},{"kind":"Field","name":{"kind":"Name","value":"mostInstalledVersion"}},{"kind":"Field","name":{"kind":"Name","value":"versionDistribution"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/apps/graphql/mutations/upgradeRegistrationApplications.ts b/packages/twenty-front/src/modules/settings/admin-panel/apps/graphql/mutations/upgradeRegistrationApplications.ts index 0a3f9dd41c..a57c8ea15e 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/apps/graphql/mutations/upgradeRegistrationApplications.ts +++ b/packages/twenty-front/src/modules/settings/admin-panel/apps/graphql/mutations/upgradeRegistrationApplications.ts @@ -3,9 +3,11 @@ import { gql } from '@apollo/client'; export const UPGRADE_REGISTRATION_APPLICATIONS = gql` mutation UpgradeRegistrationApplications( $applicationRegistrationId: String! + $batchSize: Int ) { upgradeRegistrationApplications( applicationRegistrationId: $applicationRegistrationId + batchSize: $batchSize ) } `; diff --git a/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationGeneralStats.tsx b/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationGeneralStats.tsx index f84064c1be..eec49f32a8 100644 --- a/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationGeneralStats.tsx +++ b/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationGeneralStats.tsx @@ -1,5 +1,6 @@ import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { useModal } from '@/ui/layout/modal/hooks/useModal'; import { useMutation } from '@apollo/client/react'; @@ -20,9 +21,16 @@ const StyledUpgradeContainer = styled.div` margin-top: ${themeCssVariables.spacing[6]}; `; +const StyledBatchSizeContainer = styled.div` + margin-top: ${themeCssVariables.spacing[4]}; + text-align: left; +`; + const UPGRADE_INSTALLATIONS_MODAL_ID = 'upgrade-registration-applications-modal'; +const DEFAULT_UPGRADE_BATCH_SIZE = 5; + export const SettingsApplicationRegistrationGeneralStats = ({ registration, }: { @@ -34,17 +42,29 @@ export const SettingsApplicationRegistrationGeneralStats = ({ const { openModal, closeModal } = useModal(); const [isUpgrading, setIsUpgrading] = useState(false); + const [batchSizeInput, setBatchSizeInput] = useState( + String(DEFAULT_UPGRADE_BATCH_SIZE), + ); const [upgradeApplications] = useMutation( UpgradeRegistrationApplicationsDocument, { client: apolloAdminClient }, ); + const parsedBatchSize = Number.parseInt(batchSizeInput, 10); + const batchSize = + Number.isNaN(parsedBatchSize) || parsedBatchSize < 1 + ? DEFAULT_UPGRADE_BATCH_SIZE + : parsedBatchSize; + const handleUpgrade = async () => { setIsUpgrading(true); try { await upgradeApplications({ - variables: { applicationRegistrationId: registration.id }, + variables: { + applicationRegistrationId: registration.id, + batchSize, + }, }); enqueueSuccessSnackBar({ message: t`Upgrade started. Existing installations will be upgraded to the latest version in the background.`, @@ -83,7 +103,24 @@ export const SettingsApplicationRegistrationGeneralStats = ({ + {t`This will upgrade "${registration.name}" to its latest available version on every workspace that already has it installed. Workspaces without the app are not affected. It runs as a background job and may take a while. Continue?`} + + + + + } onConfirmClick={handleUpgrade} confirmButtonText={t`Upgrade`} confirmButtonAccent="blue" diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts index 016aeff5e4..78b5cad865 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts @@ -566,6 +566,8 @@ export class AdminPanelResolver { @Mutation(() => Boolean) async upgradeRegistrationApplications( @Args('applicationRegistrationId') applicationRegistrationId: string, + @Args('batchSize', { type: () => Int, nullable: true }) + batchSize?: number, ): Promise { await this.applicationRegistrationService.findOneByIdGlobal( applicationRegistrationId, @@ -573,7 +575,11 @@ export class AdminPanelResolver { await this.workspaceQueueService.add( UPGRADE_APPLICATIONS_JOB_NAME, - { applicationRegistrationId, onlyAutoUpgrade: false }, + { + applicationRegistrationId, + onlyAutoUpgrade: false, + ...(isDefined(batchSize) ? { batchSize } : {}), + }, { id: `${UPGRADE_APPLICATIONS_JOB_NAME}-${applicationRegistrationId}`, }, // Avoids triggering multiple pending jobs for the same app diff --git a/packages/twenty-server/src/engine/core-modules/application/application-upgrade/application-upgrade.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-upgrade/application-upgrade.service.ts index 75f5397860..e56ca837db 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-upgrade/application-upgrade.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-upgrade/application-upgrade.service.ts @@ -21,7 +21,7 @@ const npmPackageMetadataSchema = z.object({ version: z.string(), }); -const UPGRADE_APPLICATIONS_BATCH_SIZE = 20; +const UPGRADE_APPLICATIONS_DEFAULT_BATCH_SIZE = 5; @Injectable() export class ApplicationUpgradeService { @@ -114,9 +114,11 @@ export class ApplicationUpgradeService { async upgradeAllApplications({ applicationRegistrationId, onlyAutoUpgrade = false, + batchSize = UPGRADE_APPLICATIONS_DEFAULT_BATCH_SIZE, }: { applicationRegistrationId: string; onlyAutoUpgrade?: boolean; + batchSize?: number; }): Promise { const appRegistration = await this.appRegistrationRepository.findOneOrFail({ where: { id: applicationRegistrationId }, @@ -139,14 +141,16 @@ export class ApplicationUpgradeService { (application) => application.version !== targetVersion, ); + const sanitizedBatchSize = Math.max(1, Math.floor(batchSize)); + for ( let batchStart = 0; batchStart < applicationsToUpgrade.length; - batchStart += UPGRADE_APPLICATIONS_BATCH_SIZE + batchStart += sanitizedBatchSize ) { const batch = applicationsToUpgrade.slice( batchStart, - batchStart + UPGRADE_APPLICATIONS_BATCH_SIZE, + batchStart + sanitizedBatchSize, ); await Promise.all( diff --git a/packages/twenty-server/src/engine/core-modules/application/jobs/upgrade-applications.job-constants.ts b/packages/twenty-server/src/engine/core-modules/application/jobs/upgrade-applications.job-constants.ts index 1846a4b0cb..1b3afb47f6 100644 --- a/packages/twenty-server/src/engine/core-modules/application/jobs/upgrade-applications.job-constants.ts +++ b/packages/twenty-server/src/engine/core-modules/application/jobs/upgrade-applications.job-constants.ts @@ -3,4 +3,5 @@ export const UPGRADE_APPLICATIONS_JOB_NAME = 'UpgradeApplicationsJob'; export type UpgradeApplicationsJobData = { applicationRegistrationId: string; onlyAutoUpgrade: boolean; + batchSize?: number; }; diff --git a/packages/twenty-server/src/engine/core-modules/application/jobs/upgrade-applications.job.ts b/packages/twenty-server/src/engine/core-modules/application/jobs/upgrade-applications.job.ts index 0e208071d7..161dda47d2 100644 --- a/packages/twenty-server/src/engine/core-modules/application/jobs/upgrade-applications.job.ts +++ b/packages/twenty-server/src/engine/core-modules/application/jobs/upgrade-applications.job.ts @@ -18,6 +18,7 @@ export class UpgradeApplicationsJob { await this.applicationUpgradeService.upgradeAllApplications({ applicationRegistrationId: data.applicationRegistrationId, onlyAutoUpgrade: data.onlyAutoUpgrade, + batchSize: data.batchSize, }); } }