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:
@@ -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 };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user