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
@@ -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 TimelineCalendarEventParticipant {
@ObjectType('TimelineCalendarEventParticipant')
export class TimelineCalendarEventParticipantDTO {
@Field(() => UUIDScalarType, { nullable: true })
personId: string | null;
@@ -1,11 +1,11 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { TimelineCalendarEventParticipant } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-event-participant.dto';
import { TimelineCalendarEventParticipantDTO } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-event-participant.dto';
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
@ObjectType()
class LinkMetadata {
@ObjectType('LinkMetadata')
class LinkMetadataDTO {
@Field()
label: string;
@@ -13,20 +13,20 @@ class LinkMetadata {
url: string;
}
@ObjectType()
export class LinksMetadata {
@ObjectType('LinksMetadata')
export class LinksMetadataDTO {
@Field()
primaryLinkLabel: string;
@Field()
primaryLinkUrl: string;
@Field(() => [LinkMetadata], { nullable: true })
secondaryLinks: LinkMetadata[] | null;
@Field(() => [LinkMetadataDTO], { nullable: true })
secondaryLinks: LinkMetadataDTO[] | null;
}
@ObjectType()
export class TimelineCalendarEvent {
@ObjectType('TimelineCalendarEvent')
export class TimelineCalendarEventDTO {
@Field(() => UUIDScalarType)
id: string;
@@ -54,11 +54,11 @@ export class TimelineCalendarEvent {
@Field()
conferenceSolution: string;
@Field(() => LinksMetadata)
conferenceLink: LinksMetadata;
@Field(() => LinksMetadataDTO)
conferenceLink: LinksMetadataDTO;
@Field(() => [TimelineCalendarEventParticipant])
participants: TimelineCalendarEventParticipant[];
@Field(() => [TimelineCalendarEventParticipantDTO])
participants: TimelineCalendarEventParticipantDTO[];
@Field(() => CalendarChannelVisibility)
visibility: CalendarChannelVisibility;
@@ -1,12 +1,12 @@
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { TimelineCalendarEvent } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-event.dto';
import { TimelineCalendarEventDTO } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-event.dto';
@ObjectType()
export class TimelineCalendarEventsWithTotal {
@ObjectType('TimelineCalendarEventsWithTotal')
export class TimelineCalendarEventsWithTotalDTO {
@Field(() => Int)
totalNumberOfCalendarEvents: number;
@Field(() => [TimelineCalendarEvent])
timelineCalendarEvents: TimelineCalendarEvent[];
@Field(() => [TimelineCalendarEventDTO])
timelineCalendarEvents: TimelineCalendarEventDTO[];
}
@@ -5,7 +5,7 @@ import { Max } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { TIMELINE_CALENDAR_EVENTS_MAX_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
import { TimelineCalendarEventsWithTotal } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto';
import { TimelineCalendarEventsWithTotalDTO } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto';
import { TimelineCalendarEventService } from 'src/engine/core-modules/calendar/timeline-calendar-event.service';
import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@@ -50,13 +50,13 @@ class GetTimelineCalendarEventsFromOpportunityIdArgs {
}
@UseGuards(WorkspaceAuthGuard)
@Resolver(() => TimelineCalendarEventsWithTotal)
@Resolver(() => TimelineCalendarEventsWithTotalDTO)
export class TimelineCalendarEventResolver {
constructor(
private readonly timelineCalendarEventService: TimelineCalendarEventService,
) {}
@Query(() => TimelineCalendarEventsWithTotal)
@Query(() => TimelineCalendarEventsWithTotalDTO)
async getTimelineCalendarEventsFromPersonId(
@Args()
{ personId, page, pageSize }: GetTimelineCalendarEventsFromPersonIdArgs,
@@ -73,7 +73,7 @@ export class TimelineCalendarEventResolver {
return timelineCalendarEvents;
}
@Query(() => TimelineCalendarEventsWithTotal)
@Query(() => TimelineCalendarEventsWithTotalDTO)
async getTimelineCalendarEventsFromCompanyId(
@Args()
{ companyId, page, pageSize }: GetTimelineCalendarEventsFromCompanyIdArgs,
@@ -90,7 +90,7 @@ export class TimelineCalendarEventResolver {
return timelineCalendarEvents;
}
@Query(() => TimelineCalendarEventsWithTotal)
@Query(() => TimelineCalendarEventsWithTotalDTO)
async getTimelineCalendarEventsFromOpportunityId(
@Args()
{
@@ -5,7 +5,7 @@ import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/
import { Any } from 'typeorm';
import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
import { type TimelineCalendarEventsWithTotal } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto';
import { type TimelineCalendarEventsWithTotalDTO } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
@@ -27,7 +27,7 @@ export class TimelineCalendarEventService {
personIds: string[];
page: number;
pageSize: number;
}): Promise<TimelineCalendarEventsWithTotal> {
}): Promise<TimelineCalendarEventsWithTotalDTO> {
const offset = (page - 1) * pageSize;
const calendarEventRepository =
@@ -167,7 +167,7 @@ export class TimelineCalendarEventService {
companyId: string;
page: number;
pageSize: number;
}): Promise<TimelineCalendarEventsWithTotal> {
}): Promise<TimelineCalendarEventsWithTotalDTO> {
const personRepository =
await this.twentyORMManager.getRepository<PersonWorkspaceEntity>(
'person',
@@ -211,7 +211,7 @@ export class TimelineCalendarEventService {
opportunityId: string;
page: number;
pageSize: number;
}): Promise<TimelineCalendarEventsWithTotal> {
}): Promise<TimelineCalendarEventsWithTotalDTO> {
const opportunityRepository =
await this.twentyORMManager.getRepository<OpportunityWorkspaceEntity>(
'opportunity',