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 <martin@twenty.com>
This commit is contained in:
martmull
2026-07-21 16:22:19 +02:00
committed by GitHub
parent 07be5e0892
commit e14c32015f
7 changed files with 60 additions and 7 deletions
@@ -590,6 +590,7 @@ export type MutationUpdateWorkspaceFeatureFlagArgs = {
export type MutationUpgradeRegistrationApplicationsArgs = {
applicationRegistrationId: Scalars['String']['input'];
batchSize?: InputMaybe<Scalars['Int']['input']>;
};
export type PaginatedApplicationRegistrations = {
@@ -1115,6 +1116,7 @@ export type UpdateAdminApplicationRegistrationVariableMutation = { __typename?:
export type UpgradeRegistrationApplicationsMutationVariables = Exact<{
applicationRegistrationId: Scalars['String']['input'];
batchSize?: InputMaybe<Scalars['Int']['input']>;
}>;
@@ -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<SyncMarketplaceCatalogMutation, SyncMarketplaceCatalogMutationVariables>;
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<UpdateAdminApplicationRegistrationMutation, UpdateAdminApplicationRegistrationMutationVariables>;
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<UpdateAdminApplicationRegistrationVariableMutation, UpdateAdminApplicationRegistrationVariableMutationVariables>;
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<UpgradeRegistrationApplicationsMutation, UpgradeRegistrationApplicationsMutationVariables>;
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<UpgradeRegistrationApplicationsMutation, UpgradeRegistrationApplicationsMutationVariables>;
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<FindAdminApplicationRegistrationClaimsQuery, FindAdminApplicationRegistrationClaimsQueryVariables>;
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<FindAdminApplicationRegistrationInstalledWorkspacesQuery, FindAdminApplicationRegistrationInstalledWorkspacesQueryVariables>;
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<FindAdminApplicationRegistrationStatsQuery, FindAdminApplicationRegistrationStatsQueryVariables>;
@@ -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
)
}
`;
@@ -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 = ({
<ConfirmationModal
modalInstanceId={UPGRADE_INSTALLATIONS_MODAL_ID}
title={t`Upgrade existing installations`}
subtitle={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?`}
subtitle={
<>
{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?`}
<StyledBatchSizeContainer>
<SettingsTextInput
instanceId="upgrade-registration-applications-batch-size"
dataTestId="upgrade-registration-applications-batch-size"
label={t`Batch size`}
type="number"
value={batchSizeInput}
onChange={setBatchSizeInput}
placeholder={String(DEFAULT_UPGRADE_BATCH_SIZE)}
fullWidth
disableHotkeys
/>
</StyledBatchSizeContainer>
</>
}
onConfirmClick={handleUpgrade}
confirmButtonText={t`Upgrade`}
confirmButtonAccent="blue"
@@ -566,6 +566,8 @@ export class AdminPanelResolver {
@Mutation(() => Boolean)
async upgradeRegistrationApplications(
@Args('applicationRegistrationId') applicationRegistrationId: string,
@Args('batchSize', { type: () => Int, nullable: true })
batchSize?: number,
): Promise<boolean> {
await this.applicationRegistrationService.findOneByIdGlobal(
applicationRegistrationId,
@@ -573,7 +575,11 @@ export class AdminPanelResolver {
await this.workspaceQueueService.add<UpgradeApplicationsJobData>(
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
@@ -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<void> {
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(
@@ -3,4 +3,5 @@ export const UPGRADE_APPLICATIONS_JOB_NAME = 'UpgradeApplicationsJob';
export type UpgradeApplicationsJobData = {
applicationRegistrationId: string;
onlyAutoUpgrade: boolean;
batchSize?: number;
};
@@ -18,6 +18,7 @@ export class UpgradeApplicationsJob {
await this.applicationUpgradeService.upgradeAllApplications({
applicationRegistrationId: data.applicationRegistrationId,
onlyAutoUpgrade: data.onlyAutoUpgrade,
batchSize: data.batchSize,
});
}
}