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
@@ -1,7 +1,7 @@
import { gql } from '@apollo/client';
export const BILLING_PRICE_LICENSED_FRAGMENT = gql`
fragment BillingPriceLicensedFragment on BillingPriceLicensedDTO {
fragment BillingPriceLicensedFragment on BillingPriceLicensed {
stripePriceId
unitAmount
recurringInterval
@@ -1,7 +1,7 @@
import { gql } from '@apollo/client';
export const BILLING_PRICE_METERED_FRAGMENT = gql`
fragment BillingPriceMeteredFragment on BillingPriceMeteredDTO {
fragment BillingPriceMeteredFragment on BillingPriceMetered {
priceUsageType
recurringInterval
stripePriceId
@@ -1,8 +1,8 @@
import {
type BillingPriceLicensedDto,
type BillingPriceMeteredDto,
} from '~/generated/graphql';
import { usePlans } from '@/billing/hooks/usePlans';
import {
type BillingPriceLicensed,
type BillingPriceMetered,
} from '~/generated/graphql';
export const useAllBillingPrices = () => {
const { listPlans } = usePlans();
@@ -13,7 +13,7 @@ export const useAllBillingPrices = () => {
({ prices }) => prices,
);
})
.flat(2) as Array<BillingPriceLicensedDto | BillingPriceMeteredDto>;
.flat(2) as Array<BillingPriceLicensed | BillingPriceMetered>;
return { allBillingPrices };
};
@@ -1,9 +1,9 @@
import { isDefined } from 'twenty-shared/utils';
import {
type BillingPriceLicensedDto,
type BillingPriceMeteredDto,
type BillingPriceLicensed,
type BillingPriceMetered,
BillingUsageType,
} from '~/generated/graphql';
import { isDefined } from 'twenty-shared/utils';
import { useAllBillingPrices } from './useAllBillingPrices';
export const usePriceAndBillingUsageByPriceId = () => {
@@ -13,18 +13,18 @@ export const usePriceAndBillingUsageByPriceId = () => {
priceId: string,
):
| {
price: BillingPriceLicensedDto;
price: BillingPriceLicensed;
billingUsage: BillingUsageType.LICENSED;
}
| {
price: BillingPriceMeteredDto;
price: BillingPriceMetered;
billingUsage: BillingUsageType.METERED;
} => {
const licensed = allBillingPrices.find(
(p) =>
p.priceUsageType === BillingUsageType.LICENSED &&
p.stripePriceId === priceId,
) as BillingPriceLicensedDto | undefined;
) as BillingPriceLicensed | undefined;
if (isDefined(licensed))
return { price: licensed, billingUsage: BillingUsageType.LICENSED };
@@ -33,7 +33,7 @@ export const usePriceAndBillingUsageByPriceId = () => {
(p) =>
p.priceUsageType === BillingUsageType.METERED &&
p.stripePriceId === priceId,
) as BillingPriceMeteredDto | undefined;
) as BillingPriceMetered | undefined;
if (isDefined(metered))
return { price: metered, billingUsage: BillingUsageType.METERED };
@@ -1,10 +1,10 @@
import { useNextBillingPhase } from '@/billing/hooks/useNextBillingPhase';
import { usePriceAndBillingUsageByPriceId } from '@/billing/hooks/usePriceAndBillingUsageByPriceId';
import { type MeteredBillingPrice } from '@/billing/types/billing-price-tiers.type';
import {
type BillingPriceLicensedDto,
type BillingPriceLicensed,
BillingUsageType,
} from '~/generated/graphql';
import { type MeteredBillingPrice } from '@/billing/types/billing-price-tiers.type';
import { usePriceAndBillingUsageByPriceId } from '@/billing/hooks/usePriceAndBillingUsageByPriceId';
import { useNextBillingPhase } from '@/billing/hooks/useNextBillingPhase';
export const useSplitPhaseItemsInPrices = () => {
const { nextBillingPhase } = useNextBillingPhase();
@@ -26,7 +26,7 @@ export const useSplitPhaseItemsInPrices = () => {
},
{} as {
nextMereredPrice: MeteredBillingPrice | undefined;
nextLicensedPrice: BillingPriceLicensedDto | undefined;
nextLicensedPrice: BillingPriceLicensed | undefined;
},
);
@@ -1,4 +1,4 @@
import type { BillingPriceMeteredDto } from '~/generated/graphql';
import type { BillingPriceMetered } from '~/generated/graphql';
export type BillingPriceTiers = [
{
@@ -14,6 +14,6 @@ export type BillingPriceTiers = [
];
// graphql does not support tuple so we need to create a new type
export type MeteredBillingPrice = Omit<BillingPriceMeteredDto, 'tiers'> & {
export type MeteredBillingPrice = Omit<BillingPriceMetered, 'tiers'> & {
tiers: BillingPriceTiers;
};