Paginate admin panel app registrations list (#22734)
## Context The `findAllApplicationRegistrations` query on the admin panel Apps page (`/settings/admin-panel#apps`) loaded every application registration at once, with search and filtering done client-side. ## Changes **Server** - `findAllApplicationRegistrations` now takes `limit` / `offset` / `searchTerm` / `isPreInstalledOnly` args and returns a `PaginatedApplicationRegistrations` object (`registrations`, `totalCount`, `hasMore`), following the same pattern as `getQueueJobs`. - `ApplicationRegistrationService.findAll` uses `findAndCount` with `take`/`skip`, and moves the search (name, source package, universal identifier via `ILIKE`) and the pre-installed filter into the SQL query, mirroring how `getInstalledWorkspacesGlobal` filters installed workspaces. **Frontend** - `SettingsAdminApps` passes the page, the debounced search term (300ms, like the installed workspaces table), and the pre-installed toggle as query variables instead of filtering client-side. - Adds a Previous / Next pagination footer (25 per page) matching the queue jobs table, shown only when there is more than one page. - The "unconfigured first" ordering is kept within each page (`isConfigured` is a dataloader-resolved field, so it can't be sorted in SQL). ## Notes - Regenerated `generated-admin/graphql.ts` follows in a subsequent commit. --- _Generated by [Claude Code](https://claude.ai/code/session_015erumgPozkbNA3zPeKrrFW)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22734?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
+22
-10
@@ -55,6 +55,7 @@ import { ApplicationRegistrationService } from 'src/engine/core-modules/applicat
|
||||
import { ApplicationRegistrationInstalledWorkspacesDTO } from 'src/engine/core-modules/application/application-registration/dtos/application-registration-installed-workspaces.dto';
|
||||
import { ApplicationRegistrationStatsDTO } from 'src/engine/core-modules/application/application-registration/dtos/application-registration-stats.dto';
|
||||
import { FindApplicationRegistrationInstalledWorkspacesInput } from 'src/engine/core-modules/application/application-registration/dtos/find-application-registration-installed-workspaces.input';
|
||||
import { PaginatedApplicationRegistrationsDTO } from 'src/engine/core-modules/application/application-registration/dtos/paginated-application-registrations.dto';
|
||||
import { UpdateApplicationRegistrationInput } from 'src/engine/core-modules/application/application-registration/dtos/update-application-registration.input';
|
||||
import {
|
||||
BACKFILL_APPLICATION_INSTALLATION_JOB_NAME,
|
||||
@@ -105,8 +106,6 @@ import { ModelsDevProviderSuggestionDTO } from './dtos/models-dev-provider-sugge
|
||||
import { QueueMetricsDataDTO } from './dtos/queue-metrics-data.dto';
|
||||
import { SetMaintenanceModeInput } from './dtos/set-maintenance-mode.input';
|
||||
|
||||
const INSTALLED_WORKSPACES_PAGE_SIZE = 10;
|
||||
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
@AdminResolver()
|
||||
@UseFilters(
|
||||
@@ -480,11 +479,23 @@ export class AdminPanelResolver {
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Query(() => [ApplicationRegistrationEntity])
|
||||
async findAllApplicationRegistrations(): Promise<
|
||||
ApplicationRegistrationEntity[]
|
||||
> {
|
||||
return this.applicationRegistrationService.findAll();
|
||||
@Query(() => PaginatedApplicationRegistrationsDTO)
|
||||
async findAllApplicationRegistrations(
|
||||
@Args('limit', { type: () => Int, nullable: true, defaultValue: 25 })
|
||||
limit: number,
|
||||
@Args('offset', { type: () => Int, nullable: true, defaultValue: 0 })
|
||||
offset: number,
|
||||
@Args('searchTerm', { type: () => String, nullable: true })
|
||||
searchTerm?: string,
|
||||
@Args('isPreInstalledOnly', { type: () => Boolean, nullable: true })
|
||||
isPreInstalledOnly?: boolean,
|
||||
): Promise<PaginatedApplicationRegistrationsDTO> {
|
||||
return this.applicationRegistrationService.findAll({
|
||||
limit,
|
||||
offset,
|
||||
searchTerm,
|
||||
isPreInstalledOnly,
|
||||
});
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@@ -838,14 +849,15 @@ export class AdminPanelResolver {
|
||||
@Args('input')
|
||||
{
|
||||
id,
|
||||
page,
|
||||
limit,
|
||||
offset,
|
||||
searchTerm,
|
||||
}: FindApplicationRegistrationInstalledWorkspacesInput,
|
||||
): Promise<ApplicationRegistrationInstalledWorkspacesDTO> {
|
||||
return this.applicationRegistrationService.getInstalledWorkspacesGlobal(
|
||||
id,
|
||||
page ?? 1,
|
||||
INSTALLED_WORKSPACES_PAGE_SIZE,
|
||||
limit,
|
||||
offset,
|
||||
searchTerm,
|
||||
);
|
||||
}
|
||||
|
||||
+61
-15
@@ -19,6 +19,7 @@ import {
|
||||
ApplicationRegistrationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application-registration/application-registration.exception';
|
||||
import { type ApplicationRegistrationInstalledWorkspacesDTO } from 'src/engine/core-modules/application/application-registration/dtos/application-registration-installed-workspaces.dto';
|
||||
import { type PaginatedApplicationRegistrationsDTO } from 'src/engine/core-modules/application/application-registration/dtos/paginated-application-registrations.dto';
|
||||
import { type ApplicationRegistrationStatsDTO } from 'src/engine/core-modules/application/application-registration/dtos/application-registration-stats.dto';
|
||||
import { type CreateApplicationRegistrationInput } from 'src/engine/core-modules/application/application-registration/dtos/create-application-registration.input';
|
||||
import { type PublicApplicationRegistrationDTO } from 'src/engine/core-modules/application/application-registration/dtos/public-application-registration.dto';
|
||||
@@ -38,6 +39,8 @@ import { MARKETPLACE_VETTED_APPLICATIONS } from 'src/engine/core-modules/applica
|
||||
|
||||
const BCRYPT_SALT_ROUNDS = 10;
|
||||
|
||||
const MAX_APPLICATION_REGISTRATIONS_PAGE_SIZE = 100;
|
||||
|
||||
const APPLICATION_REGISTRATION_WITHOUT_MANIFEST_SELECT: (keyof ApplicationRegistrationEntity)[] =
|
||||
[
|
||||
'id',
|
||||
@@ -119,11 +122,57 @@ export class ApplicationRegistrationService {
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(): Promise<ApplicationRegistrationEntity[]> {
|
||||
return this.applicationRegistrationRepository.find({
|
||||
select: APPLICATION_REGISTRATION_WITHOUT_MANIFEST_SELECT,
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
async findAll({
|
||||
limit,
|
||||
offset,
|
||||
searchTerm,
|
||||
isPreInstalledOnly,
|
||||
}: {
|
||||
limit: number;
|
||||
offset: number;
|
||||
searchTerm?: string;
|
||||
isPreInstalledOnly?: boolean;
|
||||
}): Promise<PaginatedApplicationRegistrationsDTO> {
|
||||
const safeLimit = Math.min(
|
||||
Math.max(limit, 1),
|
||||
MAX_APPLICATION_REGISTRATIONS_PAGE_SIZE,
|
||||
);
|
||||
const safeOffset = Math.max(offset, 0);
|
||||
|
||||
const trimmedSearch = searchTerm?.trim();
|
||||
|
||||
const queryBuilder = this.applicationRegistrationRepository
|
||||
.createQueryBuilder('registration')
|
||||
.select(
|
||||
APPLICATION_REGISTRATION_WITHOUT_MANIFEST_SELECT.map(
|
||||
(column) => `registration.${column}`,
|
||||
),
|
||||
)
|
||||
.orderBy('registration.createdAt', 'DESC')
|
||||
.addOrderBy('registration.id', 'ASC')
|
||||
.skip(safeOffset)
|
||||
.take(safeLimit);
|
||||
|
||||
if (isPreInstalledOnly === true) {
|
||||
queryBuilder.andWhere('registration."isPreInstalled" = true');
|
||||
}
|
||||
|
||||
if (isDefined(trimmedSearch) && trimmedSearch.length > 0) {
|
||||
queryBuilder.andWhere(
|
||||
`(registration.name ILIKE :searchTerm
|
||||
OR registration."sourcePackage" ILIKE :searchTerm
|
||||
OR registration."universalIdentifier"::text ILIKE :searchTerm)`,
|
||||
{ searchTerm: `%${trimmedSearch}%` },
|
||||
);
|
||||
}
|
||||
|
||||
const [registrations, totalCount] = await queryBuilder.getManyAndCount();
|
||||
|
||||
return {
|
||||
registrations,
|
||||
totalCount,
|
||||
hasMore: safeOffset + registrations.length < totalCount,
|
||||
};
|
||||
}
|
||||
|
||||
async findOneById(
|
||||
@@ -582,29 +631,26 @@ export class ApplicationRegistrationService {
|
||||
// across all workspaces, so ownership is not enforced.
|
||||
async getInstalledWorkspacesGlobal(
|
||||
applicationRegistrationId: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
limit: number,
|
||||
offset: number,
|
||||
searchTerm?: string,
|
||||
): Promise<ApplicationRegistrationInstalledWorkspacesDTO> {
|
||||
await this.findOneByIdGlobal(applicationRegistrationId);
|
||||
|
||||
return this.computeInstalledWorkspaces(
|
||||
applicationRegistrationId,
|
||||
page,
|
||||
pageSize,
|
||||
limit,
|
||||
offset,
|
||||
searchTerm,
|
||||
);
|
||||
}
|
||||
|
||||
private async computeInstalledWorkspaces(
|
||||
applicationRegistrationId: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
limit: number,
|
||||
offset: number,
|
||||
searchTerm?: string,
|
||||
): Promise<ApplicationRegistrationInstalledWorkspacesDTO> {
|
||||
const safePage = page < 1 ? 1 : page;
|
||||
const offset = (safePage - 1) * pageSize;
|
||||
|
||||
const trimmedSearch = searchTerm?.trim();
|
||||
|
||||
const where: FindOptionsWhere<ApplicationEntity> = {
|
||||
@@ -628,7 +674,7 @@ export class ApplicationRegistrationService {
|
||||
relations: { workspace: true },
|
||||
order: { workspace: { displayName: 'ASC' }, id: 'ASC' },
|
||||
skip: offset,
|
||||
take: pageSize,
|
||||
take: limit,
|
||||
});
|
||||
|
||||
const workspaces = applications.map((application) => ({
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const INSTALLED_WORKSPACES_DEFAULT_LIMIT = 10;
|
||||
export const INSTALLED_WORKSPACES_DEFAULT_OFFSET = 0;
|
||||
+21
-4
@@ -1,6 +1,18 @@
|
||||
import { Field, InputType, Int } from '@nestjs/graphql';
|
||||
|
||||
import { IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator';
|
||||
import {
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Max,
|
||||
MaxLength,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
|
||||
import {
|
||||
INSTALLED_WORKSPACES_DEFAULT_LIMIT,
|
||||
INSTALLED_WORKSPACES_DEFAULT_OFFSET,
|
||||
} from 'src/engine/core-modules/application/application-registration/constants/installed-workspaces-pagination.constant';
|
||||
|
||||
@InputType()
|
||||
export class FindApplicationRegistrationInstalledWorkspacesInput {
|
||||
@@ -8,11 +20,16 @@ export class FindApplicationRegistrationInstalledWorkspacesInput {
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@Field(() => Int, { defaultValue: 1 })
|
||||
@Field(() => Int, { defaultValue: INSTALLED_WORKSPACES_DEFAULT_LIMIT })
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@IsOptional()
|
||||
page?: number;
|
||||
@Max(100)
|
||||
limit: number;
|
||||
|
||||
@Field(() => Int, { defaultValue: INSTALLED_WORKSPACES_DEFAULT_OFFSET })
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
offset: number;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { Field, Int, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
|
||||
@ObjectType('PaginatedApplicationRegistrations')
|
||||
export class PaginatedApplicationRegistrationsDTO {
|
||||
@Field(() => [ApplicationRegistrationEntity])
|
||||
registrations: ApplicationRegistrationEntity[];
|
||||
|
||||
@Field(() => Int)
|
||||
totalCount: number;
|
||||
|
||||
@Field(() => Boolean)
|
||||
hasMore: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user