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:
+2
-2
@@ -2,8 +2,8 @@ import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType()
|
||||
export class TimelineThreadParticipant {
|
||||
@ObjectType('TimelineThreadParticipant')
|
||||
export class TimelineThreadParticipantDTO {
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
personId: string | null;
|
||||
|
||||
|
||||
+6
-6
@@ -1,11 +1,11 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { TimelineThreadParticipant } from 'src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto';
|
||||
import { TimelineThreadParticipantDTO } from 'src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
@ObjectType()
|
||||
export class TimelineThread {
|
||||
@ObjectType('TimelineThread')
|
||||
export class TimelineThreadDTO {
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@@ -16,10 +16,10 @@ export class TimelineThread {
|
||||
visibility: MessageChannelVisibility;
|
||||
|
||||
@Field()
|
||||
firstParticipant: TimelineThreadParticipant;
|
||||
firstParticipant: TimelineThreadParticipantDTO;
|
||||
|
||||
@Field(() => [TimelineThreadParticipant])
|
||||
lastTwoParticipants: TimelineThreadParticipant[];
|
||||
@Field(() => [TimelineThreadParticipantDTO])
|
||||
lastTwoParticipants: TimelineThreadParticipantDTO[];
|
||||
|
||||
@Field()
|
||||
lastMessageReceivedAt: Date;
|
||||
|
||||
+5
-5
@@ -1,12 +1,12 @@
|
||||
import { Field, Int, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { TimelineThread } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto';
|
||||
import { TimelineThreadDTO } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto';
|
||||
|
||||
@ObjectType()
|
||||
export class TimelineThreadsWithTotal {
|
||||
@ObjectType('TimelineThreadsWithTotal')
|
||||
export class TimelineThreadsWithTotalDTO {
|
||||
@Field(() => Int)
|
||||
totalNumberOfThreads: number;
|
||||
|
||||
@Field(() => [TimelineThread])
|
||||
timelineThreads: TimelineThread[];
|
||||
@Field(() => [TimelineThreadDTO])
|
||||
timelineThreads: TimelineThreadDTO[];
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/messaging/constants/messaging.constants';
|
||||
import { type TimelineThreadsWithTotal } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto';
|
||||
import { type TimelineThreadsWithTotalDTO } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto';
|
||||
import { TimelineMessagingService } from 'src/engine/core-modules/messaging/services/timeline-messaging.service';
|
||||
import { formatThreads } from 'src/engine/core-modules/messaging/utils/format-threads.util';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
@@ -20,7 +20,7 @@ export class GetMessagesService {
|
||||
personIds: string[],
|
||||
page = 1,
|
||||
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
|
||||
): Promise<TimelineThreadsWithTotal> {
|
||||
): Promise<TimelineThreadsWithTotalDTO> {
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const { messageThreads, totalNumberOfThreads } =
|
||||
@@ -67,7 +67,7 @@ export class GetMessagesService {
|
||||
companyId: string,
|
||||
page = 1,
|
||||
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
|
||||
): Promise<TimelineThreadsWithTotal> {
|
||||
): Promise<TimelineThreadsWithTotalDTO> {
|
||||
const personRepository =
|
||||
await this.twentyORMManager.getRepository<PersonWorkspaceEntity>(
|
||||
'person',
|
||||
@@ -105,7 +105,7 @@ export class GetMessagesService {
|
||||
opportunityId: string,
|
||||
page = 1,
|
||||
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
|
||||
): Promise<TimelineThreadsWithTotal> {
|
||||
): Promise<TimelineThreadsWithTotalDTO> {
|
||||
const opportunityRepository =
|
||||
await this.twentyORMManager.getRepository<OpportunityWorkspaceEntity>(
|
||||
'opportunity',
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import { type TimelineThread } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto';
|
||||
import { type TimelineThreadDTO } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { type MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||
@@ -18,7 +18,7 @@ export class TimelineMessagingService {
|
||||
pageSize: number,
|
||||
): Promise<{
|
||||
messageThreads: Omit<
|
||||
TimelineThread,
|
||||
TimelineThreadDTO,
|
||||
| 'firstParticipant'
|
||||
| 'lastTwoParticipants'
|
||||
| 'participantCount'
|
||||
|
||||
+5
-5
@@ -5,7 +5,7 @@ import { Max } from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { TIMELINE_THREADS_MAX_PAGE_SIZE } from 'src/engine/core-modules/messaging/constants/messaging.constants';
|
||||
import { TimelineThreadsWithTotal } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto';
|
||||
import { TimelineThreadsWithTotalDTO } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto';
|
||||
import { GetMessagesService } from 'src/engine/core-modules/messaging/services/get-messages.service';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
@@ -55,14 +55,14 @@ class GetTimelineThreadsFromOpportunityIdArgs {
|
||||
}
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
||||
@Resolver(() => TimelineThreadsWithTotal)
|
||||
@Resolver(() => TimelineThreadsWithTotalDTO)
|
||||
export class TimelineMessagingResolver {
|
||||
constructor(
|
||||
private readonly getMessagesFromPersonIdsService: GetMessagesService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
@Query(() => TimelineThreadsWithTotal)
|
||||
@Query(() => TimelineThreadsWithTotalDTO)
|
||||
async getTimelineThreadsFromPersonId(
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@@ -88,7 +88,7 @@ export class TimelineMessagingResolver {
|
||||
return timelineThreads;
|
||||
}
|
||||
|
||||
@Query(() => TimelineThreadsWithTotal)
|
||||
@Query(() => TimelineThreadsWithTotalDTO)
|
||||
async getTimelineThreadsFromCompanyId(
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@@ -114,7 +114,7 @@ export class TimelineMessagingResolver {
|
||||
return timelineThreads;
|
||||
}
|
||||
|
||||
@Query(() => TimelineThreadsWithTotal)
|
||||
@Query(() => TimelineThreadsWithTotalDTO)
|
||||
async getTimelineThreadsFromOpportunityId(
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { type TimelineThreadParticipant } from 'src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto';
|
||||
import { type TimelineThreadParticipantDTO } from 'src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto';
|
||||
import { filterActiveParticipants } from 'src/engine/core-modules/messaging/utils/filter-active-participants.util';
|
||||
import { formatThreadParticipant } from 'src/engine/core-modules/messaging/utils/format-thread-participant.util';
|
||||
import { type MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||
@@ -6,8 +6,8 @@ import { type MessageParticipantWorkspaceEntity } from 'src/modules/messaging/co
|
||||
export const extractParticipantSummary = (
|
||||
messageParticipants: MessageParticipantWorkspaceEntity[],
|
||||
): {
|
||||
firstParticipant: TimelineThreadParticipant;
|
||||
lastTwoParticipants: TimelineThreadParticipant[];
|
||||
firstParticipant: TimelineThreadParticipantDTO;
|
||||
lastTwoParticipants: TimelineThreadParticipantDTO[];
|
||||
participantCount: number;
|
||||
} => {
|
||||
const activeMessageParticipants =
|
||||
@@ -23,7 +23,7 @@ export const extractParticipantSummary = (
|
||||
threadParticipant.handle !== firstParticipant.handle,
|
||||
);
|
||||
|
||||
const lastTwoParticipants: TimelineThreadParticipant[] = [];
|
||||
const lastTwoParticipants: TimelineThreadParticipantDTO[] = [];
|
||||
|
||||
const lastParticipant =
|
||||
activeMessageParticipantsWithoutFirstParticipant.slice(-1)[0];
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import { type TimelineThreadParticipant } from 'src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto';
|
||||
import { type TimelineThreadParticipantDTO } from 'src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto';
|
||||
import { type MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||
|
||||
export const formatThreadParticipant = (
|
||||
threadParticipant: MessageParticipantWorkspaceEntity,
|
||||
): TimelineThreadParticipant => ({
|
||||
): TimelineThreadParticipantDTO => ({
|
||||
personId: threadParticipant.personId,
|
||||
workspaceMemberId: threadParticipant.workspaceMemberId,
|
||||
firstName:
|
||||
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||
|
||||
import { type TimelineThread } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto';
|
||||
import { type TimelineThreadDTO } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto';
|
||||
import { extractParticipantSummary } from 'src/engine/core-modules/messaging/utils/extract-participant-summary.util';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { type MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||
|
||||
export const formatThreads = (
|
||||
threads: Omit<
|
||||
TimelineThread,
|
||||
TimelineThreadDTO,
|
||||
| 'firstParticipant'
|
||||
| 'lastTwoParticipants'
|
||||
| 'participantCount'
|
||||
@@ -20,7 +20,7 @@ export const formatThreads = (
|
||||
threadVisibilityByThreadId: {
|
||||
[key: string]: MessageChannelVisibility;
|
||||
},
|
||||
): TimelineThread[] => {
|
||||
): TimelineThreadDTO[] => {
|
||||
return threads.map((thread) => {
|
||||
const visibility = threadVisibilityByThreadId[thread.id];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user