Add queue management dashboard (#15202)

Adds a comprehensive queue management interface to the admin panel for
viewing and managing background jobs.

**Features:**
- Queue detail pages showing paginated job lists (50 per page)
- Filter jobs by state: completed, failed, active, waiting, delayed,
paused
- Checkbox selection with bulk actions (delete jobs, retry failed jobs)
- Per-job dropdown menu for individual retry/delete
- Expandable rows showing error messages, stack traces, and job data
- Relative timestamps with hover tooltips
- Display attempt counts on failed jobs
- Dynamic retention policy info from backend

**Changes:**
- Backend: New AdminPanelQueueService with GraphQL endpoints for job
listing, retry, and delete
- Frontend: Queue detail page with QueueJobsTable component
- Updated retention policy: completed jobs kept 4 hours, failed jobs
kept 7 days (max 1000 each)
- Added JobState enum for type safety

<img width="634" height="696" alt="Screenshot_2025-10-20_at_11 45 25"
src="https://github.com/user-attachments/assets/c67bcd27-26cf-47f5-9575-3cd5684d006b"
/>
<img width="484" height="680" alt="Screenshot_2025-10-20_at_11 45 14"
src="https://github.com/user-attachments/assets/68725cc6-b3ec-4098-99ca-f9a717d6f8f1"
/>
<img width="490" height="643" alt="Screenshot_2025-10-20_at_11 45 05"
src="https://github.com/user-attachments/assets/b68a5809-33ff-4452-b48b-741aff7f1dd6"
/>



<img width="685" height="662" alt="Screenshot 2025-10-20 at 13 15 01"
src="https://github.com/user-attachments/assets/eeb5207b-de5c-4b18-bdde-392892053dab"
/>
This commit is contained in:
Félix Malfait
2025-10-21 16:02:50 +02:00
committed by GitHub
parent 27f50c4f4e
commit f7421c5fc0
144 changed files with 2914 additions and 955 deletions
@@ -5,7 +5,7 @@ import { type Redis } from 'ioredis';
import { AdminPanelHealthService } from 'src/engine/core-modules/admin-panel/admin-panel-health.service';
import { HEALTH_INDICATORS } from 'src/engine/core-modules/admin-panel/constants/health-indicators.constants';
import { type SystemHealth } from 'src/engine/core-modules/admin-panel/dtos/system-health.dto';
import { type SystemHealthDTO } from 'src/engine/core-modules/admin-panel/dtos/system-health.dto';
import { AdminPanelHealthServiceStatus } from 'src/engine/core-modules/admin-panel/enums/admin-panel-health-service-status.enum';
import { QueueMetricsTimeRange } from 'src/engine/core-modules/admin-panel/enums/queue-metrics-time-range.enum';
import { HEALTH_ERROR_MESSAGES } from 'src/engine/core-modules/health/constants/health-error-messages.constants';
@@ -40,6 +40,7 @@ describe('AdminPanelHealthService', () => {
appHealth = { isHealthy: jest.fn() } as any;
redisClient = {
getClient: jest.fn().mockReturnValue({} as Redis),
getQueueClient: jest.fn().mockReturnValue({} as Redis),
} as any;
twentyConfigService = { get: jest.fn() } as any;
@@ -150,7 +151,7 @@ describe('AdminPanelHealthService', () => {
const result = await service.getSystemHealthStatus();
const expected: SystemHealth = {
const expected: SystemHealthDTO = {
services: [
{
...HEALTH_INDICATORS[HealthIndicatorId.database],
@@ -372,6 +373,7 @@ describe('AdminPanelHealthService', () => {
beforeEach(() => {
jest.clearAllMocks();
redisClient.getClient.mockReturnValue({} as Redis);
redisClient.getQueueClient.mockReturnValue({} as Redis);
(Queue as unknown as jest.Mock).mockImplementation(() => mockQueue);
});
@@ -7,9 +7,9 @@ import {
import { Queue } from 'bullmq';
import { HEALTH_INDICATORS } from 'src/engine/core-modules/admin-panel/constants/health-indicators.constants';
import { type AdminPanelHealthServiceData } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-health-service-data.dto';
import { type QueueMetricsData } from 'src/engine/core-modules/admin-panel/dtos/queue-metrics-data.dto';
import { type SystemHealth } from 'src/engine/core-modules/admin-panel/dtos/system-health.dto';
import { type AdminPanelHealthServiceDataDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-health-service-data.dto';
import { type QueueMetricsDataDTO } from 'src/engine/core-modules/admin-panel/dtos/queue-metrics-data.dto';
import { type SystemHealthDTO } from 'src/engine/core-modules/admin-panel/dtos/system-health.dto';
import { AdminPanelHealthServiceStatus } from 'src/engine/core-modules/admin-panel/enums/admin-panel-health-service-status.enum';
import { QueueMetricsTimeRange } from 'src/engine/core-modules/admin-panel/enums/queue-metrics-time-range.enum';
import { HealthIndicatorId } from 'src/engine/core-modules/health/enums/health-indicator-id.enum';
@@ -117,7 +117,7 @@ export class AdminPanelHealthService {
async getIndicatorHealthStatus(
indicatorId: HealthIndicatorId,
): Promise<AdminPanelHealthServiceData> {
): Promise<AdminPanelHealthServiceDataDTO> {
const healthIndicator = this.healthIndicators[indicatorId];
if (!healthIndicator) {
@@ -145,7 +145,7 @@ export class AdminPanelHealthService {
return indicatorStatus;
}
async getSystemHealthStatus(): Promise<SystemHealth> {
async getSystemHealthStatus(): Promise<SystemHealthDTO> {
const [
databaseResult,
redisResult,
@@ -198,8 +198,8 @@ export class AdminPanelHealthService {
async getQueueMetrics(
queueName: MessageQueue,
timeRange: QueueMetricsTimeRange = QueueMetricsTimeRange.OneDay,
): Promise<QueueMetricsData> {
const redis = this.redisClient.getClient();
): Promise<QueueMetricsDataDTO> {
const redis = this.redisClient.getQueueClient();
const queue = new Queue(queueName, { connection: redis });
try {
@@ -325,7 +325,7 @@ export class AdminPanelHealthService {
timeRange: QueueMetricsTimeRange,
queueName: MessageQueue,
queueDetails: WorkerQueueHealth | null,
): QueueMetricsData {
): QueueMetricsDataDTO {
try {
return {
queueName,
@@ -0,0 +1,246 @@
import { Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { Queue } from 'bullmq';
import { type JobState as BullMQJobState } from 'bullmq/dist/esm/types';
import {
bullMQToJobStateEnum,
JobStateEnum,
jobStateEnumToBullMQ,
} from 'src/engine/core-modules/admin-panel/enums/job-state.enum';
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { QUEUE_RETENTION } from 'src/engine/core-modules/message-queue/constants/queue-retention.constants';
import { type MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { RedisClientService } from 'src/engine/core-modules/redis-client/redis-client.service';
type JobOperationResult = {
jobId: string;
success: boolean;
error?: string;
};
@Injectable()
export class AdminPanelQueueService {
constructor(private readonly redisClient: RedisClientService) {}
async getQueueJobs(
queueName: MessageQueue,
state: JobStateEnum,
limit = 50,
offset = 0,
) {
const redis = this.redisClient.getQueueClient();
const queue = new Queue(queueName, { connection: redis });
try {
const validLimit = Math.min(Math.max(1, limit), 200);
const validOffset = Math.max(0, offset);
const start = validOffset;
const end = validOffset + validLimit - 1;
// Convert GraphQL enum to BullMQ state
const bullMQState = jobStateEnumToBullMQ[state];
const jobs = await queue.getJobs([bullMQState], start, end, false);
const transformedJobs = await Promise.all(
jobs.map(async (job) => {
const jobBullMQState = (await job.getState()) as BullMQJobState;
return {
id: job.id!,
name: job.name,
data: job.data,
state: bullMQToJobStateEnum[jobBullMQState],
timestamp: job.timestamp,
failedReason: job.failedReason,
processedOn: job.processedOn,
finishedOn: job.finishedOn,
attemptsMade: job.attemptsMade,
returnValue: job.returnValue,
logs: undefined,
stackTrace: job.stackTrace,
};
}),
);
const hasMore = jobs.length === validLimit;
const jobCounts = await queue.getJobCounts(
'completed',
'failed',
'active',
'waiting',
'delayed',
'prioritized',
'waiting-children',
);
const totalCountForState = (() => {
switch (state) {
case JobStateEnum.COMPLETED:
return jobCounts.completed ?? 0;
case JobStateEnum.FAILED:
return jobCounts.failed ?? 0;
case JobStateEnum.ACTIVE:
return jobCounts.active ?? 0;
case JobStateEnum.WAITING:
return jobCounts.waiting ?? 0;
case JobStateEnum.DELAYED:
return jobCounts.delayed ?? 0;
case JobStateEnum.PRIORITIZED:
return jobCounts.prioritized ?? 0;
case JobStateEnum.WAITING_CHILDREN:
return jobCounts['waiting-children'] ?? 0;
default:
return jobs.length;
}
})();
return {
jobs: transformedJobs,
count: jobs.length,
totalCount: totalCountForState,
hasMore,
retentionConfig: { ...QUEUE_RETENTION },
};
} catch (error) {
throw new InternalServerError(
`Failed to fetch jobs from queue ${queueName}: ${error instanceof Error ? error.message : String(error)}`,
{
userFriendlyMessage: msg`Failed to load queue jobs. Please try again later.`,
},
);
} finally {
await queue.close();
}
}
async retryJobs(
queueName: MessageQueue,
jobIds: string[],
): Promise<{
retriedCount: number;
results: JobOperationResult[];
}> {
const redis = this.redisClient.getQueueClient();
const queue = new Queue(queueName, { connection: redis });
try {
if (jobIds.length === 0) {
await queue.retryJobs({ state: 'failed' });
return { retriedCount: -1, results: [] };
}
const results: JobOperationResult[] = [];
let retriedCount = 0;
for (const jobId of jobIds) {
const job = await queue.getJob(jobId);
if (!job) {
results.push({
jobId,
success: false,
error: 'Job not found',
});
continue;
}
const state = await job.getState();
if (state !== 'failed') {
results.push({
jobId,
success: false,
error: `Job is not in failed state (current state: ${state})`,
});
continue;
}
try {
await job.retry();
retriedCount++;
results.push({
jobId,
success: true,
});
} catch (error) {
results.push({
jobId,
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
}
return { retriedCount, results };
} catch (error) {
throw new InternalServerError(
`Failed to retry jobs in queue ${queueName}: ${error instanceof Error ? error.message : String(error)}`,
{
userFriendlyMessage: msg`Failed to retry jobs. Please try again later.`,
},
);
} finally {
await queue.close();
}
}
async deleteJobs(
queueName: MessageQueue,
jobIds: string[],
): Promise<{
deletedCount: number;
results: JobOperationResult[];
}> {
const redis = this.redisClient.getQueueClient();
const queue = new Queue(queueName, { connection: redis });
try {
const results: JobOperationResult[] = [];
let deletedCount = 0;
for (const jobId of jobIds) {
const job = await queue.getJob(jobId);
if (!job) {
results.push({
jobId,
success: false,
error: 'Job not found',
});
continue;
}
try {
await job.remove();
deletedCount++;
results.push({
jobId,
success: true,
});
} catch (error) {
results.push({
jobId,
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
}
return { deletedCount, results };
} catch (error) {
throw new InternalServerError(
`Failed to delete jobs in queue ${queueName}: ${error instanceof Error ? error.message : String(error)}`,
{
userFriendlyMessage: msg`Failed to delete jobs. Please try again later.`,
},
);
} finally {
await queue.close();
}
}
}
@@ -3,6 +3,7 @@ import { TerminusModule } from '@nestjs/terminus';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminPanelHealthService } from 'src/engine/core-modules/admin-panel/admin-panel-health.service';
import { AdminPanelQueueService } from 'src/engine/core-modules/admin-panel/admin-panel-queue.service';
import { AdminPanelResolver } from 'src/engine/core-modules/admin-panel/admin-panel.resolver';
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
@@ -32,7 +33,12 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
ImpersonationModule,
PermissionsModule,
],
providers: [AdminPanelResolver, AdminPanelService, AdminPanelHealthService],
providers: [
AdminPanelResolver,
AdminPanelService,
AdminPanelHealthService,
AdminPanelQueueService,
],
exports: [AdminPanelService],
})
export class AdminPanelModule {}
@@ -1,17 +1,22 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { AdminPanelHealthService } from 'src/engine/core-modules/admin-panel/admin-panel-health.service';
import { AdminPanelQueueService } from 'src/engine/core-modules/admin-panel/admin-panel-queue.service';
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
import { ConfigVariable } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
import { ConfigVariableDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
import { ConfigVariablesOutput } from 'src/engine/core-modules/admin-panel/dtos/config-variables.output';
import { SystemHealth } from 'src/engine/core-modules/admin-panel/dtos/system-health.dto';
import { DeleteJobsResponseDTO } from 'src/engine/core-modules/admin-panel/dtos/delete-jobs-response.dto';
import { QueueJobsResponseDTO } from 'src/engine/core-modules/admin-panel/dtos/queue-jobs-response.dto';
import { RetryJobsResponseDTO } from 'src/engine/core-modules/admin-panel/dtos/retry-jobs-response.dto';
import { SystemHealthDTO } from 'src/engine/core-modules/admin-panel/dtos/system-health.dto';
import { UpdateWorkspaceFeatureFlagInput } from 'src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input';
import { UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity';
import { UserLookupInput } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.input';
import { VersionInfo } from 'src/engine/core-modules/admin-panel/dtos/version-info.dto';
import { VersionInfoDTO } from 'src/engine/core-modules/admin-panel/dtos/version-info.dto';
import { JobStateEnum } from 'src/engine/core-modules/admin-panel/enums/job-state.enum';
import { QueueMetricsTimeRange } from 'src/engine/core-modules/admin-panel/enums/queue-metrics-time-range.enum';
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
import { FeatureFlagException } from 'src/engine/core-modules/feature-flag/feature-flag.exception';
@@ -29,8 +34,8 @@ import { ServerLevelImpersonateGuard } from 'src/engine/guards/server-level-impe
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { AdminPanelHealthServiceData } from './dtos/admin-panel-health-service-data.dto';
import { QueueMetricsData } from './dtos/queue-metrics-data.dto';
import { AdminPanelHealthServiceDataDTO } from './dtos/admin-panel-health-service-data.dto';
import { QueueMetricsDataDTO } from './dtos/queue-metrics-data.dto';
@UsePipes(ResolverValidationPipe)
@Resolver()
@@ -43,6 +48,7 @@ export class AdminPanelResolver {
constructor(
private adminService: AdminPanelService,
private adminPanelHealthService: AdminPanelHealthService,
private adminPanelQueueService: AdminPanelQueueService,
private featureFlagService: FeatureFlagService,
private readonly twentyConfigService: TwentyConfigService,
) {}
@@ -84,34 +90,34 @@ export class AdminPanelResolver {
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => SystemHealth)
async getSystemHealthStatus(): Promise<SystemHealth> {
@Query(() => SystemHealthDTO)
async getSystemHealthStatus(): Promise<SystemHealthDTO> {
return this.adminPanelHealthService.getSystemHealthStatus();
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => AdminPanelHealthServiceData)
@Query(() => AdminPanelHealthServiceDataDTO)
async getIndicatorHealthStatus(
@Args('indicatorId', {
type: () => HealthIndicatorId,
})
indicatorId: HealthIndicatorId,
): Promise<AdminPanelHealthServiceData> {
): Promise<AdminPanelHealthServiceDataDTO> {
return this.adminPanelHealthService.getIndicatorHealthStatus(indicatorId);
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => QueueMetricsData)
@Query(() => QueueMetricsDataDTO)
async getQueueMetrics(
@Args('queueName', { type: () => String })
queueName: string,
@Args('timeRange', {
nullable: true,
defaultValue: QueueMetricsTimeRange.OneDay,
defaultValue: QueueMetricsTimeRange.OneHour,
type: () => QueueMetricsTimeRange,
})
timeRange: QueueMetricsTimeRange = QueueMetricsTimeRange.OneHour,
): Promise<QueueMetricsData> {
): Promise<QueueMetricsDataDTO> {
return await this.adminPanelHealthService.getQueueMetrics(
queueName as MessageQueue,
timeRange,
@@ -119,16 +125,16 @@ export class AdminPanelResolver {
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => VersionInfo)
async versionInfo(): Promise<VersionInfo> {
@Query(() => VersionInfoDTO)
async versionInfo(): Promise<VersionInfoDTO> {
return this.adminService.getVersionInfo();
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => ConfigVariable)
@Query(() => ConfigVariableDTO)
async getDatabaseConfigVariable(
@Args('key', { type: () => String }) key: keyof ConfigVariables,
): Promise<ConfigVariable> {
): Promise<ConfigVariableDTO> {
this.twentyConfigService.validateConfigVariableExists(key as string);
return this.adminService.getConfigVariable(key);
@@ -167,4 +173,52 @@ export class AdminPanelResolver {
return true;
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => QueueJobsResponseDTO)
async getQueueJobs(
@Args('queueName', { type: () => String })
queueName: string,
@Args('state', { type: () => JobStateEnum })
state: JobStateEnum,
@Args('limit', { type: () => Int, nullable: true, defaultValue: 50 })
limit?: number,
@Args('offset', { type: () => Int, nullable: true, defaultValue: 0 })
offset?: number,
): Promise<QueueJobsResponseDTO> {
return await this.adminPanelQueueService.getQueueJobs(
queueName as MessageQueue,
state,
limit,
offset,
);
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Mutation(() => RetryJobsResponseDTO)
async retryJobs(
@Args('queueName', { type: () => String })
queueName: string,
@Args('jobIds', { type: () => [String] })
jobIds: string[],
): Promise<RetryJobsResponseDTO> {
return await this.adminPanelQueueService.retryJobs(
queueName as MessageQueue,
jobIds,
);
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Mutation(() => DeleteJobsResponseDTO)
async deleteJobs(
@Args('queueName', { type: () => String })
queueName: string,
@Args('jobIds', { type: () => [String] })
jobIds: string[],
): Promise<DeleteJobsResponseDTO> {
return await this.adminPanelQueueService.deleteJobs(
queueName as MessageQueue,
jobIds,
);
}
}
@@ -7,11 +7,11 @@ import semver from 'semver';
import { Repository } from 'typeorm';
import * as z from 'zod';
import { type ConfigVariable } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
import { type ConfigVariablesGroupData } from 'src/engine/core-modules/admin-panel/dtos/config-variables-group.dto';
import { type ConfigVariableDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
import { type ConfigVariablesGroupDataDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variables-group.dto';
import { type ConfigVariablesOutput } from 'src/engine/core-modules/admin-panel/dtos/config-variables.output';
import { type UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity';
import { type VersionInfo } from 'src/engine/core-modules/admin-panel/dtos/version-info.dto';
import { type VersionInfoDTO } from 'src/engine/core-modules/admin-panel/dtos/version-info.dto';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import {
AuthException,
@@ -114,14 +114,14 @@ export class AdminPanelService {
getConfigVariablesGrouped(): ConfigVariablesOutput {
const rawEnvVars = this.twentyConfigService.getAll();
const groupedData = new Map<ConfigVariablesGroup, ConfigVariable[]>();
const groupedData = new Map<ConfigVariablesGroup, ConfigVariableDTO[]>();
for (const [varName, { value, metadata, source }] of Object.entries(
rawEnvVars,
)) {
const { group, description } = metadata;
const envVar: ConfigVariable = {
const envVar: ConfigVariableDTO = {
name: varName,
description,
value: value ?? null,
@@ -139,7 +139,9 @@ export class AdminPanelService {
groupedData.get(group)?.push(envVar);
}
const groups: ConfigVariablesGroupData[] = Array.from(groupedData.entries())
const groups: ConfigVariablesGroupDataDTO[] = Array.from(
groupedData.entries(),
)
.sort((a, b) => {
const positionA = CONFIG_VARIABLES_GROUP_METADATA[a[0]].position;
const positionB = CONFIG_VARIABLES_GROUP_METADATA[b[0]].position;
@@ -156,7 +158,7 @@ export class AdminPanelService {
return { groups };
}
getConfigVariable(key: string): ConfigVariable {
getConfigVariable(key: string): ConfigVariableDTO {
const variableWithMetadata =
this.twentyConfigService.getVariableWithMetadata(
key as keyof ConfigVariables,
@@ -180,7 +182,7 @@ export class AdminPanelService {
};
}
async getVersionInfo(): Promise<VersionInfo> {
async getVersionInfo(): Promise<VersionInfoDTO> {
const currentVersion = this.twentyConfigService.get('APP_VERSION');
try {
@@ -1,11 +1,11 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { AdminPanelWorkerQueueHealth } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-worker-queue-health.dto';
import { AdminPanelWorkerQueueHealthDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-worker-queue-health.dto';
import { AdminPanelHealthServiceStatus } from 'src/engine/core-modules/admin-panel/enums/admin-panel-health-service-status.enum';
import { HealthIndicatorId } from 'src/engine/core-modules/health/enums/health-indicator-id.enum';
@ObjectType()
export class AdminPanelHealthServiceData {
@ObjectType('AdminPanelHealthServiceData')
export class AdminPanelHealthServiceDataDTO {
@Field(() => HealthIndicatorId)
id: HealthIndicatorId;
@@ -23,6 +23,6 @@ export class AdminPanelHealthServiceData {
@Field(() => String, { nullable: true })
details?: string;
@Field(() => [AdminPanelWorkerQueueHealth], { nullable: true })
queues?: AdminPanelWorkerQueueHealth[];
@Field(() => [AdminPanelWorkerQueueHealthDTO], { nullable: true })
queues?: AdminPanelWorkerQueueHealthDTO[];
}
@@ -2,8 +2,8 @@ import { Field, ObjectType } from '@nestjs/graphql';
import { AdminPanelHealthServiceStatus } from 'src/engine/core-modules/admin-panel/enums/admin-panel-health-service-status.enum';
@ObjectType()
export class AdminPanelWorkerQueueHealth {
@ObjectType('AdminPanelWorkerQueueHealth')
export class AdminPanelWorkerQueueHealthDTO {
@Field(() => String)
id: string;
@@ -15,8 +15,8 @@ registerEnumType(ConfigVariableType, {
name: 'ConfigVariableType',
});
@ObjectType()
export class ConfigVariable {
@ObjectType('ConfigVariable')
export class ConfigVariableDTO {
@Field()
name: string;
@@ -1,16 +1,16 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { ConfigVariable } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
import { ConfigVariableDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
registerEnumType(ConfigVariablesGroup, {
name: 'ConfigVariablesGroup',
});
@ObjectType()
export class ConfigVariablesGroupData {
@Field(() => [ConfigVariable])
variables: ConfigVariable[];
@ObjectType('ConfigVariablesGroupData')
export class ConfigVariablesGroupDataDTO {
@Field(() => [ConfigVariableDTO])
variables: ConfigVariableDTO[];
@Field(() => ConfigVariablesGroup)
name: ConfigVariablesGroup;
@@ -1,9 +1,9 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { ConfigVariablesGroupData } from 'src/engine/core-modules/admin-panel/dtos/config-variables-group.dto';
import { ConfigVariablesGroupDataDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variables-group.dto';
@ObjectType()
@ObjectType('ConfigVariablesOutput')
export class ConfigVariablesOutput {
@Field(() => [ConfigVariablesGroupData])
groups: ConfigVariablesGroupData[];
@Field(() => [ConfigVariablesGroupDataDTO])
groups: ConfigVariablesGroupDataDTO[];
}
@@ -0,0 +1,12 @@
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { JobOperationResultDTO } from 'src/engine/core-modules/admin-panel/dtos/job-operation-result.dto';
@ObjectType('DeleteJobsResponse')
export class DeleteJobsResponseDTO {
@Field(() => Int)
deletedCount: number;
@Field(() => [JobOperationResultDTO])
results: JobOperationResultDTO[];
}
@@ -1,13 +1,13 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { AuthToken } from 'src/engine/core-modules/auth/dto/token.entity';
import { WorkspaceUrlsAndId } from 'src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto';
import { WorkspaceUrlsAndIdDTO } from 'src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto';
@ObjectType()
@ObjectType('ImpersonateOutput')
export class ImpersonateOutput {
@Field(() => AuthToken)
loginToken: AuthToken;
@Field(() => WorkspaceUrlsAndId)
workspace: WorkspaceUrlsAndId;
@Field(() => WorkspaceUrlsAndIdDTO)
workspace: WorkspaceUrlsAndIdDTO;
}
@@ -0,0 +1,13 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType('JobOperationResult')
export class JobOperationResultDTO {
@Field(() => String)
jobId: string;
@Field(() => Boolean)
success: boolean;
@Field(() => String, { nullable: true })
error?: string;
}
@@ -0,0 +1,44 @@
import { Field, ObjectType } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { JobStateEnum } from 'src/engine/core-modules/admin-panel/enums/job-state.enum';
@ObjectType('QueueJob')
export class QueueJobDTO {
@Field(() => String)
id: string;
@Field(() => String)
name: string;
@Field(() => GraphQLJSON, { nullable: true })
data?: object;
@Field(() => JobStateEnum)
state: JobStateEnum;
@Field(() => Number, { nullable: true })
timestamp?: number;
@Field(() => String, { nullable: true })
failedReason?: string;
@Field(() => Number, { nullable: true })
processedOn?: number;
@Field(() => Number, { nullable: true })
finishedOn?: number;
@Field(() => Number)
attemptsMade: number;
@Field(() => GraphQLJSON, { nullable: true })
returnValue?: object;
@Field(() => [String], { nullable: true })
logs?: string[];
@Field(() => [String], { nullable: true })
stackTrace?: string[];
}
@@ -0,0 +1,22 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { QueueJobDTO } from 'src/engine/core-modules/admin-panel/dtos/queue-job.dto';
import { QueueRetentionConfigDTO } from 'src/engine/core-modules/admin-panel/dtos/queue-retention-config.dto';
@ObjectType('QueueJobsResponse')
export class QueueJobsResponseDTO {
@Field(() => [QueueJobDTO])
jobs: QueueJobDTO[];
@Field(() => Number)
count: number;
@Field(() => Number)
totalCount: number;
@Field(() => Boolean)
hasMore: boolean;
@Field(() => QueueRetentionConfigDTO)
retentionConfig: QueueRetentionConfigDTO;
}
@@ -1,7 +1,7 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class QueueMetricsDataPoint {
@ObjectType('QueueMetricsDataPoint')
export class QueueMetricsDataPointDTO {
@Field(() => Number)
x: number;
@@ -1,11 +1,11 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { QueueMetricsSeries } from 'src/engine/core-modules/admin-panel/dtos/queue-metrics-series.dto';
import { QueueMetricsSeriesDTO } from 'src/engine/core-modules/admin-panel/dtos/queue-metrics-series.dto';
import { QueueMetricsTimeRange } from 'src/engine/core-modules/admin-panel/enums/queue-metrics-time-range.enum';
import { WorkerQueueMetrics } from 'src/engine/core-modules/health/types/worker-queue-metrics.type';
@ObjectType()
export class QueueMetricsData {
@ObjectType('QueueMetricsData')
export class QueueMetricsDataDTO {
@Field(() => String)
queueName: string;
@@ -18,6 +18,6 @@ export class QueueMetricsData {
@Field(() => WorkerQueueMetrics, { nullable: true })
details: WorkerQueueMetrics | null;
@Field(() => [QueueMetricsSeries])
data: QueueMetricsSeries[];
@Field(() => [QueueMetricsSeriesDTO])
data: QueueMetricsSeriesDTO[];
}
@@ -1,12 +1,12 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { QueueMetricsDataPoint } from 'src/engine/core-modules/admin-panel/dtos/queue-metrics-data-point.dto';
import { QueueMetricsDataPointDTO } from 'src/engine/core-modules/admin-panel/dtos/queue-metrics-data-point.dto';
@ObjectType()
export class QueueMetricsSeries {
@ObjectType('QueueMetricsSeries')
export class QueueMetricsSeriesDTO {
@Field(() => String)
id: string;
@Field(() => [QueueMetricsDataPoint])
data: QueueMetricsDataPoint[];
@Field(() => [QueueMetricsDataPointDTO])
data: QueueMetricsDataPointDTO[];
}
@@ -0,0 +1,16 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType('QueueRetentionConfig')
export class QueueRetentionConfigDTO {
@Field(() => Number)
completedMaxAge: number;
@Field(() => Number)
completedMaxCount: number;
@Field(() => Number)
failedMaxAge: number;
@Field(() => Number)
failedMaxCount: number;
}
@@ -0,0 +1,12 @@
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { JobOperationResultDTO } from 'src/engine/core-modules/admin-panel/dtos/job-operation-result.dto';
@ObjectType('RetryJobsResponse')
export class RetryJobsResponseDTO {
@Field(() => Int)
retriedCount: number;
@Field(() => [JobOperationResultDTO])
results: JobOperationResultDTO[];
}
@@ -3,8 +3,8 @@ import { Field, ObjectType } from '@nestjs/graphql';
import { AdminPanelHealthServiceStatus } from 'src/engine/core-modules/admin-panel/enums/admin-panel-health-service-status.enum';
import { HealthIndicatorId } from 'src/engine/core-modules/health/enums/health-indicator-id.enum';
@ObjectType()
export class SystemHealthService {
@ObjectType('SystemHealthService')
export class SystemHealthServiceDTO {
@Field(() => HealthIndicatorId)
id: HealthIndicatorId;
@@ -15,8 +15,8 @@ export class SystemHealthService {
status: AdminPanelHealthServiceStatus;
}
@ObjectType()
export class SystemHealth {
@Field(() => [SystemHealthService])
services: SystemHealthService[];
@ObjectType('SystemHealth')
export class SystemHealthDTO {
@Field(() => [SystemHealthServiceDTO])
services: SystemHealthServiceDTO[];
}
@@ -2,10 +2,10 @@ import { Field, ObjectType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { WorkspaceUrls } from 'src/engine/core-modules/workspace/dtos/workspace-urls.dto';
import { WorkspaceUrlsDTO } from 'src/engine/core-modules/workspace/dtos/workspace-urls.dto';
@ObjectType()
class UserInfo {
@ObjectType('UserInfo')
class UserInfoDTO {
@Field(() => UUIDScalarType)
id: string;
@@ -19,8 +19,8 @@ class UserInfo {
lastName?: string;
}
@ObjectType()
class WorkspaceInfo {
@ObjectType('WorkspaceInfo')
class WorkspaceInfoDTO {
@Field(() => UUIDScalarType)
id: string;
@@ -36,21 +36,21 @@ class WorkspaceInfo {
@Field(() => Number)
totalUsers: number;
@Field(() => WorkspaceUrls)
workspaceUrls: WorkspaceUrls;
@Field(() => WorkspaceUrlsDTO)
workspaceUrls: WorkspaceUrlsDTO;
@Field(() => [UserInfo])
users: UserInfo[];
@Field(() => [UserInfoDTO])
users: UserInfoDTO[];
@Field(() => [FeatureFlag])
featureFlags: FeatureFlag[];
}
@ObjectType()
@ObjectType('UserLookup')
export class UserLookup {
@Field(() => UserInfo)
user: UserInfo;
@Field(() => UserInfoDTO)
user: UserInfoDTO;
@Field(() => [WorkspaceInfo])
workspaces: WorkspaceInfo[];
@Field(() => [WorkspaceInfoDTO])
workspaces: WorkspaceInfoDTO[];
}
@@ -1,7 +1,7 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class VersionInfo {
@ObjectType('VersionInfo')
export class VersionInfoDTO {
@Field(() => String, { nullable: true })
currentVersion?: string;
@@ -0,0 +1,40 @@
import { registerEnumType } from '@nestjs/graphql';
import { type JobState as BullMQJobState } from 'bullmq/dist/esm/types';
export enum JobStateEnum {
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
ACTIVE = 'ACTIVE',
WAITING = 'WAITING',
DELAYED = 'DELAYED',
PRIORITIZED = 'PRIORITIZED',
WAITING_CHILDREN = 'WAITING_CHILDREN',
}
registerEnumType(JobStateEnum, {
name: 'JobState',
description: 'Job state in the queue',
});
// Mapping from GraphQL enum to BullMQ state values
export const jobStateEnumToBullMQ: Record<JobStateEnum, BullMQJobState> = {
[JobStateEnum.COMPLETED]: 'completed',
[JobStateEnum.FAILED]: 'failed',
[JobStateEnum.ACTIVE]: 'active',
[JobStateEnum.WAITING]: 'waiting',
[JobStateEnum.DELAYED]: 'delayed',
[JobStateEnum.PRIORITIZED]: 'prioritized',
[JobStateEnum.WAITING_CHILDREN]: 'waiting-children',
};
// Mapping from BullMQ state values to GraphQL enum
export const bullMQToJobStateEnum: Record<BullMQJobState, JobStateEnum> = {
completed: JobStateEnum.COMPLETED,
failed: JobStateEnum.FAILED,
active: JobStateEnum.ACTIVE,
waiting: JobStateEnum.WAITING,
delayed: JobStateEnum.DELAYED,
prioritized: JobStateEnum.PRIORITIZED,
'waiting-children': JobStateEnum.WAITING_CHILDREN,
};