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:
+1
-1
@@ -5,7 +5,7 @@ import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||
|
||||
@ObjectType()
|
||||
@ObjectType('BillingPriceLicensed')
|
||||
export class BillingPriceLicensedDTO {
|
||||
@Field(() => SubscriptionInterval)
|
||||
recurringInterval: SubscriptionInterval;
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { BillingPriceTierDTO } from 'src/engine/core-modules/billing/dtos/billin
|
||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||
|
||||
@ObjectType()
|
||||
@ObjectType('BillingPriceMetered')
|
||||
export class BillingPriceMeteredDTO {
|
||||
@Field(() => [BillingPriceTierDTO])
|
||||
tiers: BillingPriceTierDTO[];
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
@ObjectType('BillingPriceTier')
|
||||
export class BillingPriceTierDTO {
|
||||
@Field(() => Number, { nullable: true })
|
||||
upTo: number | null;
|
||||
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class BillingSubscriptionSchedulePhaseItem {
|
||||
@ObjectType('BillingSubscriptionSchedulePhaseItem')
|
||||
export class BillingSubscriptionSchedulePhaseItemDTO {
|
||||
@Field(() => String)
|
||||
price: string;
|
||||
|
||||
@@ -9,14 +9,14 @@ export class BillingSubscriptionSchedulePhaseItem {
|
||||
quantity?: number;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class BillingSubscriptionSchedulePhase {
|
||||
@ObjectType('BillingSubscriptionSchedulePhase')
|
||||
export class BillingSubscriptionSchedulePhaseDTO {
|
||||
@Field(() => Number)
|
||||
start_date: number;
|
||||
|
||||
@Field(() => Number)
|
||||
end_date: number;
|
||||
|
||||
@Field(() => [BillingSubscriptionSchedulePhaseItem])
|
||||
items: Array<BillingSubscriptionSchedulePhaseItem>;
|
||||
@Field(() => [BillingSubscriptionSchedulePhaseItemDTO])
|
||||
items: Array<BillingSubscriptionSchedulePhaseItemDTO>;
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { Min } from 'class-validator';
|
||||
|
||||
@ObjectType()
|
||||
@ObjectType('BillingTrialPeriod')
|
||||
export class BillingTrialPeriodDTO {
|
||||
@Field(() => Number)
|
||||
@Min(0)
|
||||
|
||||
+4
-4
@@ -3,6 +3,7 @@
|
||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import graphqlTypeJson from 'graphql-type-json';
|
||||
import Stripe from 'stripe';
|
||||
import {
|
||||
Column,
|
||||
@@ -16,16 +17,15 @@ import {
|
||||
Relation,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import graphqlTypeJson from 'graphql-type-json';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { BillingSubscriptionSchedulePhaseDTO } from 'src/engine/core-modules/billing/dtos/billing-subscription-schedule-phase.dto';
|
||||
import { BillingSubscriptionItemDTO } from 'src/engine/core-modules/billing/dtos/outputs/billing-subscription-item.output';
|
||||
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||
import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
|
||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||
import { BillingSubscriptionSchedulePhase } from 'src/engine/core-modules/billing/dtos/billing-subscription-schedule-phase.dto';
|
||||
|
||||
registerEnumType(SubscriptionStatus, { name: 'SubscriptionStatus' });
|
||||
registerEnumType(SubscriptionInterval, { name: 'SubscriptionInterval' });
|
||||
@@ -122,9 +122,9 @@ export class BillingSubscription {
|
||||
@Column({ nullable: false, type: 'jsonb', default: {} })
|
||||
metadata: Stripe.Metadata;
|
||||
|
||||
@Field(() => [BillingSubscriptionSchedulePhase])
|
||||
@Field(() => [BillingSubscriptionSchedulePhaseDTO])
|
||||
@Column({ nullable: false, type: 'jsonb', default: [] })
|
||||
phases: Array<BillingSubscriptionSchedulePhase>;
|
||||
phases: Array<BillingSubscriptionSchedulePhaseDTO>;
|
||||
|
||||
@Column({ nullable: true, type: 'timestamptz' })
|
||||
cancelAt: Date | null;
|
||||
|
||||
+5
-5
@@ -3,21 +3,21 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
import {
|
||||
assertIsDefinedOrThrow,
|
||||
findOrThrow,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { BillingSubscriptionSchedulePhase } from 'src/engine/core-modules/billing/dtos/billing-subscription-schedule-phase.dto';
|
||||
import { BillingSubscriptionSchedulePhaseDTO } from 'src/engine/core-modules/billing/dtos/billing-subscription-schedule-phase.dto';
|
||||
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
|
||||
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
|
||||
import { normalizePriceRef } from 'src/engine/core-modules/billing/utils/normalize-price-ref.utils';
|
||||
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
|
||||
import { BillingPriceService } from 'src/engine/core-modules/billing/services/billing-price.service';
|
||||
import { normalizePriceRef } from 'src/engine/core-modules/billing/utils/normalize-price-ref.utils';
|
||||
|
||||
@Injectable()
|
||||
export class BillingSubscriptionPhaseService {
|
||||
@@ -28,7 +28,7 @@ export class BillingSubscriptionPhaseService {
|
||||
private readonly billingPriceService: BillingPriceService,
|
||||
) {}
|
||||
|
||||
async getDetailsFromPhase(phase: BillingSubscriptionSchedulePhase) {
|
||||
async getDetailsFromPhase(phase: BillingSubscriptionSchedulePhaseDTO) {
|
||||
const meteredPrice = await this.billingPriceRepository.findOneOrFail({
|
||||
where: {
|
||||
stripePriceId: findOrThrow(
|
||||
|
||||
+35
-35
@@ -3,56 +3,56 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Not, Repository } from 'typeorm';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import {
|
||||
assertIsDefinedOrThrow,
|
||||
findOrThrow,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { Not, Repository } from 'typeorm';
|
||||
|
||||
import type Stripe from 'stripe';
|
||||
|
||||
import { transformStripeSubscriptionEventToDatabaseCustomer } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-customer.util';
|
||||
import { transformStripeSubscriptionEventToDatabaseSubscriptionItem } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-subscription-item.util';
|
||||
import {
|
||||
getSubscriptionStatus,
|
||||
transformStripeSubscriptionEventToDatabaseSubscription,
|
||||
} from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-subscription.util';
|
||||
import {
|
||||
BillingException,
|
||||
BillingExceptionCode,
|
||||
} from 'src/engine/core-modules/billing/billing.exception';
|
||||
import { billingValidator } from 'src/engine/core-modules/billing/billing.validate';
|
||||
import { BillingSubscriptionSchedulePhaseDTO } from 'src/engine/core-modules/billing/dtos/billing-subscription-schedule-phase.dto';
|
||||
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
|
||||
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
|
||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { type BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||
import { BillingProductKey } from 'src/engine/core-modules/billing/enums/billing-product-key.enum';
|
||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
|
||||
import { BillingPriceService } from 'src/engine/core-modules/billing/services/billing-price.service';
|
||||
import { BillingProductService } from 'src/engine/core-modules/billing/services/billing-product.service';
|
||||
import { BillingSubscriptionPhaseService } from 'src/engine/core-modules/billing/services/billing-subscription-phase.service';
|
||||
import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/services/stripe-customer.service';
|
||||
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
|
||||
import { StripeSubscriptionScheduleService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-schedule.service';
|
||||
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
|
||||
import { BillingMeterPrice } from 'src/engine/core-modules/billing/types/billing-meter-price.type';
|
||||
import { LicensedBillingSubscriptionItem } from 'src/engine/core-modules/billing/types/billing-subscription-item.type';
|
||||
import { SubscriptionWithSchedule } from 'src/engine/core-modules/billing/types/billing-subscription-with-schedule.type';
|
||||
import { MeterBillingPriceTiers } from 'src/engine/core-modules/billing/types/meter-billing-price-tier.type';
|
||||
import { ensureFutureStartDate } from 'src/engine/core-modules/billing/utils/ensure-future-start-date.util';
|
||||
import { getOppositeInterval } from 'src/engine/core-modules/billing/utils/get-opposite-interval';
|
||||
import { getOppositePlan } from 'src/engine/core-modules/billing/utils/get-opposite-plan';
|
||||
import { getPlanKeyFromSubscription } from 'src/engine/core-modules/billing/utils/get-plan-key-from-subscription.util';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||
import { LicensedBillingSubscriptionItem } from 'src/engine/core-modules/billing/types/billing-subscription-item.type';
|
||||
import { BillingSubscriptionPhaseService } from 'src/engine/core-modules/billing/services/billing-subscription-phase.service';
|
||||
import { BillingSubscriptionSchedulePhase } from 'src/engine/core-modules/billing/dtos/billing-subscription-schedule-phase.dto';
|
||||
import { getOppositeInterval } from 'src/engine/core-modules/billing/utils/get-opposite-interval';
|
||||
import { billingValidator } from 'src/engine/core-modules/billing/billing.validate';
|
||||
import { MeterBillingPriceTiers } from 'src/engine/core-modules/billing/types/meter-billing-price-tier.type';
|
||||
import { BillingMeterPrice } from 'src/engine/core-modules/billing/types/billing-meter-price.type';
|
||||
import { BillingProductKey } from 'src/engine/core-modules/billing/enums/billing-product-key.enum';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { getOppositePlan } from 'src/engine/core-modules/billing/utils/get-opposite-plan';
|
||||
import {
|
||||
getSubscriptionStatus,
|
||||
transformStripeSubscriptionEventToDatabaseSubscription,
|
||||
} from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-subscription.util';
|
||||
import { transformStripeSubscriptionEventToDatabaseSubscriptionItem } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-subscription-item.util';
|
||||
import { transformStripeSubscriptionEventToDatabaseCustomer } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-customer.util';
|
||||
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||
import { SubscriptionWithSchedule } from 'src/engine/core-modules/billing/types/billing-subscription-with-schedule.type';
|
||||
import { ensureFutureStartDate } from 'src/engine/core-modules/billing/utils/ensure-future-start-date.util';
|
||||
import { BillingPriceService } from 'src/engine/core-modules/billing/services/billing-price.service';
|
||||
|
||||
@Injectable()
|
||||
export class BillingSubscriptionService {
|
||||
@@ -287,7 +287,7 @@ export class BillingSubscriptionService {
|
||||
|
||||
const currentPhaseDetails =
|
||||
await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
currentEditable as BillingSubscriptionSchedulePhase,
|
||||
currentEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
);
|
||||
|
||||
await this.changeMeteredPrice(
|
||||
@@ -634,12 +634,12 @@ export class BillingSubscriptionService {
|
||||
}
|
||||
const currentPhaseDetails =
|
||||
await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
currentEditable as BillingSubscriptionSchedulePhase,
|
||||
currentEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
);
|
||||
const hasNextInitially = !!nextEditable;
|
||||
const nextPhaseDetailsInitial = hasNextInitially
|
||||
? await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
nextEditable as BillingSubscriptionSchedulePhase,
|
||||
nextEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
)
|
||||
: undefined;
|
||||
const currentCap = (currentPhaseDetails.meteredPrice as BillingMeterPrice)
|
||||
@@ -734,7 +734,7 @@ export class BillingSubscriptionService {
|
||||
const hasNext = !!nextEditable;
|
||||
const nextPhaseDetails = hasNext
|
||||
? await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
nextEditable as BillingSubscriptionSchedulePhase,
|
||||
nextEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
)
|
||||
: undefined;
|
||||
const currentLicensedId = currentSnap
|
||||
@@ -906,7 +906,7 @@ export class BillingSubscriptionService {
|
||||
|
||||
const currentDetails =
|
||||
await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
currentEditable as BillingSubscriptionSchedulePhase,
|
||||
currentEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
);
|
||||
const { nextEditable } = await this.loadScheduleEditable(
|
||||
billingSubscription.stripeSubscriptionId,
|
||||
@@ -925,7 +925,7 @@ export class BillingSubscriptionService {
|
||||
|
||||
const nextDetails =
|
||||
await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
nextEditable as BillingSubscriptionSchedulePhase,
|
||||
nextEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
);
|
||||
|
||||
if (nextDetails.interval !== targetInterval) {
|
||||
@@ -1000,7 +1000,7 @@ export class BillingSubscriptionService {
|
||||
if (nextEditable && currentEditable) {
|
||||
const reloadedNextDetails =
|
||||
await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
nextEditable as BillingSubscriptionSchedulePhase,
|
||||
nextEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
);
|
||||
|
||||
const mappedNext = await this.resolvePrices({
|
||||
@@ -1048,7 +1048,7 @@ export class BillingSubscriptionService {
|
||||
|
||||
const nextDetails = hasNext
|
||||
? await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
nextEditable as BillingSubscriptionSchedulePhase,
|
||||
nextEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
@@ -1099,7 +1099,7 @@ export class BillingSubscriptionService {
|
||||
|
||||
const currentDetails =
|
||||
await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
currentEditable as BillingSubscriptionSchedulePhase,
|
||||
currentEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
);
|
||||
|
||||
const currentPlan = currentDetails.plan.planKey;
|
||||
@@ -1115,7 +1115,7 @@ export class BillingSubscriptionService {
|
||||
|
||||
const nextDetails =
|
||||
await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
nextEditable as BillingSubscriptionSchedulePhase,
|
||||
nextEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
);
|
||||
|
||||
if (nextDetails.plan.planKey !== targetPlanKey) {
|
||||
@@ -1181,7 +1181,7 @@ export class BillingSubscriptionService {
|
||||
if (nextEditable && currentEditable) {
|
||||
const nextDetails =
|
||||
await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
nextEditable as BillingSubscriptionSchedulePhase,
|
||||
nextEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
);
|
||||
|
||||
const preservedNextInterval = nextDetails?.interval ?? interval;
|
||||
@@ -1233,7 +1233,7 @@ export class BillingSubscriptionService {
|
||||
|
||||
const nextDetails = hasNext
|
||||
? await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
nextEditable as BillingSubscriptionSchedulePhase,
|
||||
nextEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user