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
+139 -53
View File
@@ -115,8 +115,8 @@ export type AgentChatThread = {
updatedAt: Scalars['DateTime'];
};
export type AgentHandoffDto = {
__typename?: 'AgentHandoffDTO';
export type AgentHandoff = {
__typename?: 'AgentHandoff';
description?: Maybe<Scalars['String']>;
id: Scalars['UUID'];
toAgent: Agent;
@@ -261,8 +261,8 @@ export type AuthorizeApp = {
redirectUrl: Scalars['String'];
};
export type AutocompleteResultDto = {
__typename?: 'AutocompleteResultDto';
export type AutocompleteResult = {
__typename?: 'AutocompleteResult';
placeId: Scalars['String'];
text: Scalars['String'];
};
@@ -331,7 +331,7 @@ export type Billing = {
__typename?: 'Billing';
billingUrl?: Maybe<Scalars['String']>;
isBillingEnabled: Scalars['Boolean'];
trialPeriods: Array<BillingTrialPeriodDto>;
trialPeriods: Array<BillingTrialPeriod>;
};
export type BillingEndTrialPeriodOutput = {
@@ -348,7 +348,7 @@ export type BillingLicensedProduct = BillingProductDto & {
images?: Maybe<Array<Scalars['String']>>;
metadata: BillingProductMetadata;
name: Scalars['String'];
prices?: Maybe<Array<BillingPriceLicensedDto>>;
prices?: Maybe<Array<BillingPriceLicensed>>;
};
export type BillingMeteredProduct = BillingProductDto & {
@@ -357,7 +357,7 @@ export type BillingMeteredProduct = BillingProductDto & {
images?: Maybe<Array<Scalars['String']>>;
metadata: BillingProductMetadata;
name: Scalars['String'];
prices?: Maybe<Array<BillingPriceMeteredDto>>;
prices?: Maybe<Array<BillingPriceMetered>>;
};
export type BillingMeteredProductUsageOutput = {
@@ -383,24 +383,24 @@ export type BillingPlanOutput = {
planKey: BillingPlanKey;
};
export type BillingPriceLicensedDto = {
__typename?: 'BillingPriceLicensedDTO';
export type BillingPriceLicensed = {
__typename?: 'BillingPriceLicensed';
priceUsageType: BillingUsageType;
recurringInterval: SubscriptionInterval;
stripePriceId: Scalars['String'];
unitAmount: Scalars['Float'];
};
export type BillingPriceMeteredDto = {
__typename?: 'BillingPriceMeteredDTO';
export type BillingPriceMetered = {
__typename?: 'BillingPriceMetered';
priceUsageType: BillingUsageType;
recurringInterval: SubscriptionInterval;
stripePriceId: Scalars['String'];
tiers: Array<BillingPriceTierDto>;
tiers: Array<BillingPriceTier>;
};
export type BillingPriceTierDto = {
__typename?: 'BillingPriceTierDTO';
export type BillingPriceTier = {
__typename?: 'BillingPriceTier';
flatAmount?: Maybe<Scalars['Float']>;
unitAmount?: Maybe<Scalars['Float']>;
upTo?: Maybe<Scalars['Float']>;
@@ -472,8 +472,8 @@ export type BillingSubscriptionSchedulePhaseItem = {
quantity?: Maybe<Scalars['Float']>;
};
export type BillingTrialPeriodDto = {
__typename?: 'BillingTrialPeriodDTO';
export type BillingTrialPeriod = {
__typename?: 'BillingTrialPeriod';
duration: Scalars['Float'];
isCreditCardRequired: Scalars['Boolean'];
};
@@ -568,27 +568,27 @@ export enum ConfigVariableType {
}
export enum ConfigVariablesGroup {
AnalyticsConfig = 'AnalyticsConfig',
AwsSesSettings = 'AwsSesSettings',
BillingConfig = 'BillingConfig',
CaptchaConfig = 'CaptchaConfig',
CloudflareConfig = 'CloudflareConfig',
EmailSettings = 'EmailSettings',
ExceptionHandler = 'ExceptionHandler',
GoogleAuth = 'GoogleAuth',
ANALYTICS_CONFIG = 'ANALYTICS_CONFIG',
AWS_SES_SETTINGS = 'AWS_SES_SETTINGS',
BILLING_CONFIG = 'BILLING_CONFIG',
CAPTCHA_CONFIG = 'CAPTCHA_CONFIG',
CLOUDFLARE_CONFIG = 'CLOUDFLARE_CONFIG',
EMAIL_SETTINGS = 'EMAIL_SETTINGS',
EXCEPTION_HANDLER = 'EXCEPTION_HANDLER',
GOOGLE_AUTH = 'GOOGLE_AUTH',
LLM = 'LLM',
Logging = 'Logging',
Metering = 'Metering',
MicrosoftAuth = 'MicrosoftAuth',
Other = 'Other',
RateLimiting = 'RateLimiting',
LOGGING = 'LOGGING',
METERING = 'METERING',
MICROSOFT_AUTH = 'MICROSOFT_AUTH',
OTHER = 'OTHER',
RATE_LIMITING = 'RATE_LIMITING',
SERVERLESS_CONFIG = 'SERVERLESS_CONFIG',
SERVER_CONFIG = 'SERVER_CONFIG',
SSL = 'SSL',
ServerConfig = 'ServerConfig',
ServerlessConfig = 'ServerlessConfig',
StorageConfig = 'StorageConfig',
SupportChatConfig = 'SupportChatConfig',
TokensDuration = 'TokensDuration',
TwoFactorAuthentication = 'TwoFactorAuthentication'
STORAGE_CONFIG = 'STORAGE_CONFIG',
SUPPORT_CHAT_CONFIG = 'SUPPORT_CHAT_CONFIG',
TOKENS_DURATION = 'TOKENS_DURATION',
TWO_FACTOR_AUTHENTICATION = 'TWO_FACTOR_AUTHENTICATION'
}
export type ConfigVariablesGroupData = {
@@ -1014,6 +1014,12 @@ export type DeleteApprovedAccessDomainInput = {
id: Scalars['UUID'];
};
export type DeleteJobsResponse = {
__typename?: 'DeleteJobsResponse';
deletedCount: Scalars['Int'];
results: Array<JobOperationResult>;
};
export type DeleteOneFieldInput = {
/** The id of the field to delete. */
id: Scalars['UUID'];
@@ -1601,6 +1607,24 @@ export type InvalidatePassword = {
success: Scalars['Boolean'];
};
export type JobOperationResult = {
__typename?: 'JobOperationResult';
error?: Maybe<Scalars['String']>;
jobId: Scalars['String'];
success: Scalars['Boolean'];
};
/** Job state in the queue */
export enum JobState {
ACTIVE = 'ACTIVE',
COMPLETED = 'COMPLETED',
DELAYED = 'DELAYED',
FAILED = 'FAILED',
PRIORITIZED = 'PRIORITIZED',
WAITING = 'WAITING',
WAITING_CHILDREN = 'WAITING_CHILDREN'
}
export type LineChartConfiguration = {
__typename?: 'LineChartConfiguration';
aggregateFieldMetadataId: Scalars['UUID'];
@@ -1635,8 +1659,8 @@ export type LinksMetadata = {
secondaryLinks?: Maybe<Array<LinkMetadata>>;
};
export type LocationDto = {
__typename?: 'LocationDto';
export type Location = {
__typename?: 'Location';
lat?: Maybe<Scalars['Float']>;
lng?: Maybe<Scalars['Float']>;
};
@@ -1721,6 +1745,7 @@ export type Mutation = {
deleteDatabaseConfigVariable: Scalars['Boolean'];
deleteEmailingDomain: Scalars['Boolean'];
deleteFile: File;
deleteJobs: DeleteJobsResponse;
deleteOneAgent: Agent;
deleteOneCronTrigger: CronTrigger;
deleteOneDatabaseEventTrigger: DatabaseEventTrigger;
@@ -1776,6 +1801,7 @@ export type Mutation = {
restorePageLayout: PageLayout;
restorePageLayoutTab: PageLayoutTab;
restorePageLayoutWidget: PageLayoutWidget;
retryJobs: RetryJobsResponse;
revokeApiKey?: Maybe<ApiKey>;
runWorkflowVersion: WorkflowRun;
saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess;
@@ -1823,10 +1849,10 @@ export type Mutation = {
updateWorkspace: Workspace;
updateWorkspaceFeatureFlag: Scalars['Boolean'];
updateWorkspaceMemberRole: WorkspaceMember;
uploadFile: SignedFileDto;
uploadImage: SignedFileDto;
uploadProfilePicture: SignedFileDto;
uploadWorkspaceLogo: SignedFileDto;
uploadFile: SignedFile;
uploadImage: SignedFile;
uploadProfilePicture: SignedFile;
uploadWorkspaceLogo: SignedFile;
upsertFieldPermissions: Array<FieldPermission>;
upsertObjectPermissions: Array<ObjectPermission>;
upsertPermissionFlags: Array<PermissionFlag>;
@@ -2110,6 +2136,12 @@ export type MutationDeleteFileArgs = {
};
export type MutationDeleteJobsArgs = {
jobIds: Array<Scalars['String']>;
queueName: Scalars['String'];
};
export type MutationDeleteOneAgentArgs = {
input: AgentIdInput;
};
@@ -2374,6 +2406,12 @@ export type MutationRestorePageLayoutWidgetArgs = {
};
export type MutationRetryJobsArgs = {
jobIds: Array<Scalars['String']>;
queueName: Scalars['String'];
};
export type MutationRevokeApiKeyArgs = {
input: RevokeApiKeyDto;
};
@@ -2818,8 +2856,8 @@ export type ObjectStandardOverrides = {
translations?: Maybe<Scalars['JSON']>;
};
export type OnDbEventDto = {
__typename?: 'OnDbEventDTO';
export type OnDbEvent = {
__typename?: 'OnDbEvent';
action: DatabaseEventAction;
eventDate: Scalars['DateTime'];
objectNameSingular: Scalars['String'];
@@ -2942,11 +2980,11 @@ export type PieChartConfiguration = {
orderBy?: Maybe<GraphOrderBy>;
};
export type PlaceDetailsResultDto = {
__typename?: 'PlaceDetailsResultDto';
export type PlaceDetailsResult = {
__typename?: 'PlaceDetailsResult';
city?: Maybe<Scalars['String']>;
country?: Maybe<Scalars['String']>;
location?: Maybe<LocationDto>;
location?: Maybe<Location>;
postcode?: Maybe<Scalars['String']>;
state?: Maybe<Scalars['String']>;
};
@@ -3009,7 +3047,7 @@ export type Query = {
field: Field;
fields: FieldConnection;
findAgentHandoffTargets: Array<Agent>;
findAgentHandoffs: Array<AgentHandoffDto>;
findAgentHandoffs: Array<AgentHandoff>;
findManyAgents: Array<Agent>;
findManyApplications: Array<Application>;
findManyCronTriggers: Array<CronTrigger>;
@@ -3025,9 +3063,9 @@ export type Query = {
findOneServerlessFunction: ServerlessFunction;
findWorkspaceFromInviteHash: Workspace;
findWorkspaceInvitations: Array<WorkspaceInvitation>;
getAddressDetails: PlaceDetailsResultDto;
getAddressDetails: PlaceDetailsResult;
getApprovedAccessDomains: Array<ApprovedAccessDomain>;
getAutoCompleteAddress: Array<AutocompleteResultDto>;
getAutoCompleteAddress: Array<AutocompleteResult>;
getAvailablePackages: Scalars['JSON'];
getConfigVariablesGrouped: ConfigVariablesOutput;
getConnectedImapSmtpCaldavAccount: ConnectedImapSmtpCaldavAccount;
@@ -3055,6 +3093,7 @@ export type Query = {
getPageLayouts: Array<PageLayout>;
getPostgresCredentials?: Maybe<PostgresCredentials>;
getPublicWorkspaceDataByDomain: PublicWorkspaceDataOutput;
getQueueJobs: QueueJobsResponse;
getQueueMetrics: QueueMetricsData;
getRoles: Array<Role>;
getSSOIdentityProviders: Array<FindAvailableSsoidpOutput>;
@@ -3289,6 +3328,14 @@ export type QueryGetPublicWorkspaceDataByDomainArgs = {
};
export type QueryGetQueueJobsArgs = {
limit?: InputMaybe<Scalars['Int']>;
offset?: InputMaybe<Scalars['Int']>;
queueName: Scalars['String'];
state: JobState;
};
export type QueryGetQueueMetricsArgs = {
queueName: Scalars['String'];
timeRange?: InputMaybe<QueueMetricsTimeRange>;
@@ -3361,6 +3408,31 @@ export type QueryWebhookArgs = {
input: GetWebhookDto;
};
export type QueueJob = {
__typename?: 'QueueJob';
attemptsMade: Scalars['Float'];
data?: Maybe<Scalars['JSON']>;
failedReason?: Maybe<Scalars['String']>;
finishedOn?: Maybe<Scalars['Float']>;
id: Scalars['String'];
logs?: Maybe<Array<Scalars['String']>>;
name: Scalars['String'];
processedOn?: Maybe<Scalars['Float']>;
returnValue?: Maybe<Scalars['JSON']>;
stackTrace?: Maybe<Array<Scalars['String']>>;
state: JobState;
timestamp?: Maybe<Scalars['Float']>;
};
export type QueueJobsResponse = {
__typename?: 'QueueJobsResponse';
count: Scalars['Float'];
hasMore: Scalars['Boolean'];
jobs: Array<QueueJob>;
retentionConfig: QueueRetentionConfig;
totalCount: Scalars['Float'];
};
export type QueueMetricsData = {
__typename?: 'QueueMetricsData';
data: Array<QueueMetricsSeries>;
@@ -3390,6 +3462,14 @@ export enum QueueMetricsTimeRange {
TwelveHours = 'TwelveHours'
}
export type QueueRetentionConfig = {
__typename?: 'QueueRetentionConfig';
completedMaxAge: Scalars['Float'];
completedMaxCount: Scalars['Float'];
failedMaxAge: Scalars['Float'];
failedMaxCount: Scalars['Float'];
};
export type Relation = {
__typename?: 'Relation';
sourceFieldMetadata: Field;
@@ -3443,6 +3523,12 @@ export type ResendEmailVerificationTokenOutput = {
success: Scalars['Boolean'];
};
export type RetryJobsResponse = {
__typename?: 'RetryJobsResponse';
results: Array<JobOperationResult>;
retriedCount: Scalars['Int'];
};
export type RevokeApiKeyDto = {
id: Scalars['UUID'];
};
@@ -3643,8 +3729,8 @@ export type SignUpOutput = {
workspace: WorkspaceUrlsAndId;
};
export type SignedFileDto = {
__typename?: 'SignedFileDTO';
export type SignedFile = {
__typename?: 'SignedFile';
path: Scalars['String'];
token: Scalars['String'];
};
@@ -3668,7 +3754,7 @@ export type SubmitFormStepInput = {
export type Subscription = {
__typename?: 'Subscription';
onDbEvent: OnDbEventDto;
onDbEvent: OnDbEvent;
};
@@ -4564,7 +4650,7 @@ export type OnDbEventSubscriptionVariables = Exact<{
}>;
export type OnDbEventSubscription = { __typename?: 'Subscription', onDbEvent: { __typename?: 'OnDbEventDTO', eventDate: string, action: DatabaseEventAction, objectNameSingular: string, updatedFields?: Array<string> | null, record: any } };
export type OnDbEventSubscription = { __typename?: 'Subscription', onDbEvent: { __typename?: 'OnDbEvent', eventDate: string, action: DatabaseEventAction, objectNameSingular: string, updatedFields?: Array<string> | null, record: any } };
export type ViewFieldFragmentFragment = { __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null };