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
@@ -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 = {
@@ -1050,6 +1050,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'];
@@ -1644,6 +1650,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'];
@@ -1678,8 +1702,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']>;
};
@@ -1765,6 +1789,7 @@ export type Mutation = {
deleteDatabaseConfigVariable: Scalars['Boolean'];
deleteEmailingDomain: Scalars['Boolean'];
deleteFile: File;
deleteJobs: DeleteJobsResponse;
deleteOneAgent: Agent;
deleteOneCronTrigger: CronTrigger;
deleteOneDatabaseEventTrigger: DatabaseEventTrigger;
@@ -1821,6 +1846,7 @@ export type Mutation = {
restorePageLayout: PageLayout;
restorePageLayoutTab: PageLayoutTab;
restorePageLayoutWidget: PageLayoutWidget;
retryJobs: RetryJobsResponse;
revokeApiKey?: Maybe<ApiKey>;
runWorkflowVersion: WorkflowRun;
saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess;
@@ -1872,10 +1898,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>;
@@ -2174,6 +2200,12 @@ export type MutationDeleteFileArgs = {
};
export type MutationDeleteJobsArgs = {
jobIds: Array<Scalars['String']>;
queueName: Scalars['String'];
};
export type MutationDeleteOneAgentArgs = {
input: AgentIdInput;
};
@@ -2443,6 +2475,12 @@ export type MutationRestorePageLayoutWidgetArgs = {
};
export type MutationRetryJobsArgs = {
jobIds: Array<Scalars['String']>;
queueName: Scalars['String'];
};
export type MutationRevokeApiKeyArgs = {
input: RevokeApiKeyDto;
};
@@ -2907,8 +2945,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'];
@@ -3031,11 +3069,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']>;
};
@@ -3098,7 +3136,7 @@ export type Query = {
field: Field;
fields: FieldConnection;
findAgentHandoffTargets: Array<Agent>;
findAgentHandoffs: Array<AgentHandoffDto>;
findAgentHandoffs: Array<AgentHandoff>;
findDistantTablesWithStatus: Array<RemoteTable>;
findManyAgents: Array<Agent>;
findManyApplications: Array<Application>;
@@ -3117,9 +3155,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;
@@ -3147,6 +3185,7 @@ export type Query = {
getPageLayouts: Array<PageLayout>;
getPostgresCredentials?: Maybe<PostgresCredentials>;
getPublicWorkspaceDataByDomain: PublicWorkspaceDataOutput;
getQueueJobs: QueueJobsResponse;
getQueueMetrics: QueueMetricsData;
getRoles: Array<Role>;
getSSOIdentityProviders: Array<FindAvailableSsoidpOutput>;
@@ -3407,6 +3446,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>;
@@ -3501,6 +3548,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>;
@@ -3530,6 +3602,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;
@@ -3597,6 +3677,12 @@ export type ResendEmailVerificationTokenOutput = {
success: Scalars['Boolean'];
};
export type RetryJobsResponse = {
__typename?: 'RetryJobsResponse';
results: Array<JobOperationResult>;
retriedCount: Scalars['Int'];
};
export type RevokeApiKeyDto = {
id: Scalars['UUID'];
};
@@ -3797,8 +3883,8 @@ export type SignUpOutput = {
workspace: WorkspaceUrlsAndId;
};
export type SignedFileDto = {
__typename?: 'SignedFileDTO';
export type SignedFile = {
__typename?: 'SignedFile';
path: Scalars['String'];
token: Scalars['String'];
};
@@ -3822,7 +3908,7 @@ export type SubmitFormStepInput = {
export type Subscription = {
__typename?: 'Subscription';
onDbEvent: OnDbEventDto;
onDbEvent: OnDbEvent;
};
@@ -4780,7 +4866,7 @@ export type FindAgentHandoffsQueryVariables = Exact<{
}>;
export type FindAgentHandoffsQuery = { __typename?: 'Query', findAgentHandoffs: Array<{ __typename?: 'AgentHandoffDTO', id: string, description?: string | null, toAgent: { __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, modelId: string, prompt: string, isCustom: boolean, createdAt: string, updatedAt: string } }> };
export type FindAgentHandoffsQuery = { __typename?: 'Query', findAgentHandoffs: Array<{ __typename?: 'AgentHandoff', id: string, description?: string | null, toAgent: { __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, modelId: string, prompt: string, isCustom: boolean, createdAt: string, updatedAt: string } }> };
export type FindManyAgentsQueryVariables = Exact<{ [key: string]: never; }>;
@@ -4847,7 +4933,7 @@ export type UploadFileMutationVariables = Exact<{
}>;
export type UploadFileMutation = { __typename?: 'Mutation', uploadFile: { __typename?: 'SignedFileDTO', path: string, token: string } };
export type UploadFileMutation = { __typename?: 'Mutation', uploadFile: { __typename?: 'SignedFile', path: string, token: string } };
export type UploadImageMutationVariables = Exact<{
file: Scalars['Upload'];
@@ -4855,7 +4941,7 @@ export type UploadImageMutationVariables = Exact<{
}>;
export type UploadImageMutation = { __typename?: 'Mutation', uploadImage: { __typename?: 'SignedFileDTO', path: string, token: string } };
export type UploadImageMutation = { __typename?: 'Mutation', uploadImage: { __typename?: 'SignedFile', path: string, token: string } };
export type AuthTokenFragmentFragment = { __typename?: 'AuthToken', token: string, expiresAt: string };
@@ -5063,9 +5149,9 @@ export type ValidatePasswordResetTokenQueryVariables = Exact<{
export type ValidatePasswordResetTokenQuery = { __typename?: 'Query', validatePasswordResetToken: { __typename?: 'ValidatePasswordResetToken', id: string, email: string } };
export type BillingPriceLicensedFragmentFragment = { __typename?: 'BillingPriceLicensedDTO', stripePriceId: string, unitAmount: number, recurringInterval: SubscriptionInterval, priceUsageType: BillingUsageType };
export type BillingPriceLicensedFragmentFragment = { __typename?: 'BillingPriceLicensed', stripePriceId: string, unitAmount: number, recurringInterval: SubscriptionInterval, priceUsageType: BillingUsageType };
export type BillingPriceMeteredFragmentFragment = { __typename?: 'BillingPriceMeteredDTO', priceUsageType: BillingUsageType, recurringInterval: SubscriptionInterval, stripePriceId: string, tiers: Array<{ __typename?: 'BillingPriceTierDTO', flatAmount?: number | null, unitAmount?: number | null, upTo?: number | null }> };
export type BillingPriceMeteredFragmentFragment = { __typename?: 'BillingPriceMetered', priceUsageType: BillingUsageType, recurringInterval: SubscriptionInterval, stripePriceId: string, tiers: Array<{ __typename?: 'BillingPriceTier', flatAmount?: number | null, unitAmount?: number | null, upTo?: number | null }> };
export type BillingSubscriptionSchedulePhaseFragmentFragment = { __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> };
@@ -5133,7 +5219,7 @@ export type GetMeteredProductsUsageQuery = { __typename?: 'Query', getMeteredPro
export type ListPlansQueryVariables = Exact<{ [key: string]: never; }>;
export type ListPlansQuery = { __typename?: 'Query', listPlans: Array<{ __typename?: 'BillingPlanOutput', planKey: BillingPlanKey, licensedProducts: Array<{ __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array<string> | null, prices?: Array<{ __typename?: 'BillingPriceLicensedDTO', stripePriceId: string, unitAmount: number, recurringInterval: SubscriptionInterval, priceUsageType: BillingUsageType }> | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } }>, meteredProducts: Array<{ __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array<string> | null, prices?: Array<{ __typename?: 'BillingPriceMeteredDTO', priceUsageType: BillingUsageType, recurringInterval: SubscriptionInterval, stripePriceId: string, tiers: Array<{ __typename?: 'BillingPriceTierDTO', flatAmount?: number | null, unitAmount?: number | null, upTo?: number | null }> }> | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } }> }> };
export type ListPlansQuery = { __typename?: 'Query', listPlans: Array<{ __typename?: 'BillingPlanOutput', planKey: BillingPlanKey, licensedProducts: Array<{ __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array<string> | null, prices?: Array<{ __typename?: 'BillingPriceLicensed', stripePriceId: string, unitAmount: number, recurringInterval: SubscriptionInterval, priceUsageType: BillingUsageType }> | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } }>, meteredProducts: Array<{ __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array<string> | null, prices?: Array<{ __typename?: 'BillingPriceMetered', priceUsageType: BillingUsageType, recurringInterval: SubscriptionInterval, stripePriceId: string, tiers: Array<{ __typename?: 'BillingPriceTier', flatAmount?: number | null, unitAmount?: number | null, upTo?: number | null }> }> | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } }> }> };
export type RemoteServerFieldsFragment = { __typename?: 'RemoteServer', id: string, createdAt: string, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: string, schema?: string | null, label: string, userMappingOptions?: { __typename?: 'UserMappingOptionsUser', user?: string | null } | null };
@@ -5357,6 +5443,22 @@ export type GetVersionInfoQueryVariables = Exact<{ [key: string]: never; }>;
export type GetVersionInfoQuery = { __typename?: 'Query', versionInfo: { __typename?: 'VersionInfo', currentVersion?: string | null, latestVersion: string } };
export type DeleteJobsMutationVariables = Exact<{
queueName: Scalars['String'];
jobIds: Array<Scalars['String']> | Scalars['String'];
}>;
export type DeleteJobsMutation = { __typename?: 'Mutation', deleteJobs: { __typename?: 'DeleteJobsResponse', deletedCount: number, results: Array<{ __typename?: 'JobOperationResult', jobId: string, success: boolean, error?: string | null }> } };
export type RetryJobsMutationVariables = Exact<{
queueName: Scalars['String'];
jobIds: Array<Scalars['String']> | Scalars['String'];
}>;
export type RetryJobsMutation = { __typename?: 'Mutation', retryJobs: { __typename?: 'RetryJobsResponse', retriedCount: number, results: Array<{ __typename?: 'JobOperationResult', jobId: string, success: boolean, error?: string | null }> } };
export type GetIndicatorHealthStatusQueryVariables = Exact<{
indicatorId: HealthIndicatorId;
}>;
@@ -5364,6 +5466,16 @@ export type GetIndicatorHealthStatusQueryVariables = Exact<{
export type GetIndicatorHealthStatusQuery = { __typename?: 'Query', getIndicatorHealthStatus: { __typename?: 'AdminPanelHealthServiceData', id: HealthIndicatorId, label: string, description: string, status: AdminPanelHealthServiceStatus, errorMessage?: string | null, details?: string | null, queues?: Array<{ __typename?: 'AdminPanelWorkerQueueHealth', id: string, queueName: string, status: AdminPanelHealthServiceStatus }> | null } };
export type GetQueueJobsQueryVariables = Exact<{
queueName: Scalars['String'];
state: JobState;
limit?: InputMaybe<Scalars['Int']>;
offset?: InputMaybe<Scalars['Int']>;
}>;
export type GetQueueJobsQuery = { __typename?: 'Query', getQueueJobs: { __typename?: 'QueueJobsResponse', count: number, totalCount: number, hasMore: boolean, jobs: Array<{ __typename?: 'QueueJob', id: string, name: string, data?: any | null, state: JobState, timestamp?: number | null, failedReason?: string | null, processedOn?: number | null, finishedOn?: number | null, attemptsMade: number, returnValue?: any | null, logs?: Array<string> | null, stackTrace?: Array<string> | null }>, retentionConfig: { __typename?: 'QueueRetentionConfig', completedMaxAge: number, completedMaxCount: number, failedMaxAge: number, failedMaxCount: number } } };
export type GetQueueMetricsQueryVariables = Exact<{
queueName: Scalars['String'];
timeRange?: InputMaybe<QueueMetricsTimeRange>;
@@ -5727,7 +5839,7 @@ export type UploadProfilePictureMutationVariables = Exact<{
}>;
export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProfilePicture: { __typename?: 'SignedFileDTO', path: string, token: string } };
export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProfilePicture: { __typename?: 'SignedFile', path: string, token: string } };
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
@@ -6162,7 +6274,7 @@ export type UploadWorkspaceLogoMutationVariables = Exact<{
}>;
export type UploadWorkspaceLogoMutation = { __typename?: 'Mutation', uploadWorkspaceLogo: { __typename?: 'SignedFileDTO', path: string, token: string } };
export type UploadWorkspaceLogoMutation = { __typename?: 'Mutation', uploadWorkspaceLogo: { __typename?: 'SignedFile', path: string, token: string } };
export type CheckCustomDomainValidRecordsMutationVariables = Exact<{ [key: string]: never; }>;
@@ -6386,7 +6498,7 @@ export const AvailableSsoIdentityProvidersFragmentFragmentDoc = gql`
}
`;
export const BillingPriceLicensedFragmentFragmentDoc = gql`
fragment BillingPriceLicensedFragment on BillingPriceLicensedDTO {
fragment BillingPriceLicensedFragment on BillingPriceLicensed {
stripePriceId
unitAmount
recurringInterval
@@ -6394,7 +6506,7 @@ export const BillingPriceLicensedFragmentFragmentDoc = gql`
}
`;
export const BillingPriceMeteredFragmentFragmentDoc = gql`
fragment BillingPriceMeteredFragment on BillingPriceMeteredDTO {
fragment BillingPriceMeteredFragment on BillingPriceMetered {
priceUsageType
recurringInterval
stripePriceId
@@ -10183,6 +10295,84 @@ export function useGetVersionInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt
export type GetVersionInfoQueryHookResult = ReturnType<typeof useGetVersionInfoQuery>;
export type GetVersionInfoLazyQueryHookResult = ReturnType<typeof useGetVersionInfoLazyQuery>;
export type GetVersionInfoQueryResult = Apollo.QueryResult<GetVersionInfoQuery, GetVersionInfoQueryVariables>;
export const DeleteJobsDocument = gql`
mutation DeleteJobs($queueName: String!, $jobIds: [String!]!) {
deleteJobs(queueName: $queueName, jobIds: $jobIds) {
deletedCount
results {
jobId
success
error
}
}
}
`;
export type DeleteJobsMutationFn = Apollo.MutationFunction<DeleteJobsMutation, DeleteJobsMutationVariables>;
/**
* __useDeleteJobsMutation__
*
* To run a mutation, you first call `useDeleteJobsMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeleteJobsMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [deleteJobsMutation, { data, loading, error }] = useDeleteJobsMutation({
* variables: {
* queueName: // value for 'queueName'
* jobIds: // value for 'jobIds'
* },
* });
*/
export function useDeleteJobsMutation(baseOptions?: Apollo.MutationHookOptions<DeleteJobsMutation, DeleteJobsMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<DeleteJobsMutation, DeleteJobsMutationVariables>(DeleteJobsDocument, options);
}
export type DeleteJobsMutationHookResult = ReturnType<typeof useDeleteJobsMutation>;
export type DeleteJobsMutationResult = Apollo.MutationResult<DeleteJobsMutation>;
export type DeleteJobsMutationOptions = Apollo.BaseMutationOptions<DeleteJobsMutation, DeleteJobsMutationVariables>;
export const RetryJobsDocument = gql`
mutation RetryJobs($queueName: String!, $jobIds: [String!]!) {
retryJobs(queueName: $queueName, jobIds: $jobIds) {
retriedCount
results {
jobId
success
error
}
}
}
`;
export type RetryJobsMutationFn = Apollo.MutationFunction<RetryJobsMutation, RetryJobsMutationVariables>;
/**
* __useRetryJobsMutation__
*
* To run a mutation, you first call `useRetryJobsMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useRetryJobsMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [retryJobsMutation, { data, loading, error }] = useRetryJobsMutation({
* variables: {
* queueName: // value for 'queueName'
* jobIds: // value for 'jobIds'
* },
* });
*/
export function useRetryJobsMutation(baseOptions?: Apollo.MutationHookOptions<RetryJobsMutation, RetryJobsMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<RetryJobsMutation, RetryJobsMutationVariables>(RetryJobsDocument, options);
}
export type RetryJobsMutationHookResult = ReturnType<typeof useRetryJobsMutation>;
export type RetryJobsMutationResult = Apollo.MutationResult<RetryJobsMutation>;
export type RetryJobsMutationOptions = Apollo.BaseMutationOptions<RetryJobsMutation, RetryJobsMutationVariables>;
export const GetIndicatorHealthStatusDocument = gql`
query GetIndicatorHealthStatus($indicatorId: HealthIndicatorId!) {
getIndicatorHealthStatus(indicatorId: $indicatorId) {
@@ -10228,6 +10418,71 @@ export function useGetIndicatorHealthStatusLazyQuery(baseOptions?: Apollo.LazyQu
export type GetIndicatorHealthStatusQueryHookResult = ReturnType<typeof useGetIndicatorHealthStatusQuery>;
export type GetIndicatorHealthStatusLazyQueryHookResult = ReturnType<typeof useGetIndicatorHealthStatusLazyQuery>;
export type GetIndicatorHealthStatusQueryResult = Apollo.QueryResult<GetIndicatorHealthStatusQuery, GetIndicatorHealthStatusQueryVariables>;
export const GetQueueJobsDocument = gql`
query GetQueueJobs($queueName: String!, $state: JobState!, $limit: Int, $offset: Int) {
getQueueJobs(
queueName: $queueName
state: $state
limit: $limit
offset: $offset
) {
jobs {
id
name
data
state
timestamp
failedReason
processedOn
finishedOn
attemptsMade
returnValue
logs
stackTrace
}
count
totalCount
hasMore
retentionConfig {
completedMaxAge
completedMaxCount
failedMaxAge
failedMaxCount
}
}
}
`;
/**
* __useGetQueueJobsQuery__
*
* To run a query within a React component, call `useGetQueueJobsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetQueueJobsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetQueueJobsQuery({
* variables: {
* queueName: // value for 'queueName'
* state: // value for 'state'
* limit: // value for 'limit'
* offset: // value for 'offset'
* },
* });
*/
export function useGetQueueJobsQuery(baseOptions: Apollo.QueryHookOptions<GetQueueJobsQuery, GetQueueJobsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetQueueJobsQuery, GetQueueJobsQueryVariables>(GetQueueJobsDocument, options);
}
export function useGetQueueJobsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetQueueJobsQuery, GetQueueJobsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetQueueJobsQuery, GetQueueJobsQueryVariables>(GetQueueJobsDocument, options);
}
export type GetQueueJobsQueryHookResult = ReturnType<typeof useGetQueueJobsQuery>;
export type GetQueueJobsLazyQueryHookResult = ReturnType<typeof useGetQueueJobsLazyQuery>;
export type GetQueueJobsQueryResult = Apollo.QueryResult<GetQueueJobsQuery, GetQueueJobsQueryVariables>;
export const GetQueueMetricsDocument = gql`
query GetQueueMetrics($queueName: String!, $timeRange: QueueMetricsTimeRange) {
getQueueMetrics(queueName: $queueName, timeRange: $timeRange) {
+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 };
@@ -350,6 +350,14 @@ const SettingsAdminIndicatorHealthStatus = lazy(() =>
})),
);
const SettingsAdminQueueDetail = lazy(() =>
import('~/pages/settings/admin-panel/SettingsAdminQueueDetail').then(
(module) => ({
default: module.SettingsAdminQueueDetail,
}),
),
);
const SettingsAdminConfigVariableDetails = lazy(() =>
import(
'~/pages/settings/admin-panel/SettingsAdminConfigVariableDetails'
@@ -636,6 +644,10 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
path={SettingsPath.AdminPanelIndicatorHealthStatus}
element={<SettingsAdminIndicatorHealthStatus />}
/>
<Route
path={SettingsPath.AdminPanelQueueDetail}
element={<SettingsAdminQueueDetail />}
/>
<Route
path={SettingsPath.AdminPanelConfigVariableDetails}
@@ -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;
};
@@ -1,5 +1,6 @@
import { Button } from 'twenty-ui/input';
import { t } from '@lingui/core/macro';
import { IconHeart, IconHeartOff } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
type PageFavoriteButtonProps = {
isFavorite: boolean;
@@ -10,7 +11,7 @@ export const PageFavoriteButton = ({
isFavorite,
onClick,
}: PageFavoriteButtonProps) => {
const title = isFavorite ? 'Remove from favorites' : 'Add to favorites';
const title = isFavorite ? t`Remove from favorites` : t`Add to favorites`;
return (
<Button
@@ -1,6 +1,6 @@
import { type WidgetAccessDenialInfo } from '@/page-layout/widgets/types/WidgetAccessDenialInfo';
import { ForbiddenFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/ForbiddenFieldDisplay';
import { t } from '@lingui/core/macro';
import { type WidgetAccessDenialInfo } from '@/page-layout/widgets/types/WidgetAccessDenialInfo';
import { plural, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { AppTooltip } from 'twenty-ui/display';
@@ -27,9 +27,10 @@ export const PageLayoutWidgetForbiddenDisplay = ({
restriction.fieldNames.length > 0
) {
const fieldsList = restriction.fieldNames.join(', ');
const fieldWord =
restriction.fieldNames.length === 1 ? 'field' : 'fields';
return t`You do not have permission to access the ${fieldsList} ${fieldWord}`;
return plural(restriction.fieldNames.length, {
one: `You do not have permission to access the ${fieldsList} field`,
other: `You do not have permission to access the ${fieldsList} fields`,
});
}
return t`You do not have permission to view this widget`;
@@ -16,7 +16,7 @@ const StyledContainer = styled.div`
gap: ${({ theme }) => theme.spacing(8)};
`;
export const ConnectedAccountHealthStatus = () => {
export const SettingsAdminConnectedAccountHealthStatus = () => {
const { indicatorHealth } = useContext(SettingsAdminIndicatorHealthContext);
const details = indicatorHealth.details;
if (!details) {
@@ -32,21 +32,24 @@ export const ConnectedAccountHealthStatus = () => {
serviceDetails.calendarSync?.status ===
AdminPanelHealthServiceStatus.OUTAGE;
const errorMessages = [];
if (isMessageSyncDown) {
errorMessages.push('Message Sync');
}
if (isCalendarSyncDown) {
errorMessages.push('Calendar Sync');
}
const getErrorMessage = () => {
if (isMessageSyncDown && isCalendarSyncDown) {
return t`Message Sync and Calendar Sync are not available because the service is down`;
}
if (isMessageSyncDown) {
return t`Message Sync is not available because the service is down`;
}
if (isCalendarSyncDown) {
return t`Calendar Sync is not available because the service is down`;
}
return null;
};
const errorMessage = getErrorMessage();
return (
<StyledContainer>
{errorMessages.length > 0 && (
<StyledErrorMessage>
{`${errorMessages.join(' and ')} ${errorMessages.length > 1 ? 'are' : 'is'} not available because the service is down`}
</StyledErrorMessage>
)}
{errorMessage && <StyledErrorMessage>{errorMessage}</StyledErrorMessage>}
{!isMessageSyncDown && serviceDetails.messageSync?.details && (
<SettingsAdminHealthAccountSyncCountersTable
@@ -0,0 +1,38 @@
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { plural, t } from '@lingui/core/macro';
type SettingsAdminDeleteJobsConfirmationModalProps = {
modalId: string;
jobCount: number;
onConfirm: () => void;
onClose?: () => void;
};
export const SettingsAdminDeleteJobsConfirmationModal = ({
modalId,
jobCount,
onConfirm,
onClose,
}: SettingsAdminDeleteJobsConfirmationModalProps) => {
const title = plural(jobCount, {
one: `Delete ${jobCount} Job`,
other: `Delete ${jobCount} Jobs`,
});
const subtitle = plural(jobCount, {
one: `This will permanently remove it from the queue. This action cannot be undone.`,
other: `This will permanently remove them from the queue. This action cannot be undone.`,
});
return (
<ConfirmationModal
modalId={modalId}
title={title}
subtitle={subtitle}
onConfirmClick={onConfirm}
onClose={onClose}
confirmButtonText={t`Delete`}
confirmButtonAccent="danger"
/>
);
};
@@ -1,5 +1,5 @@
import { SettingsAdminTabSkeletonLoader } from '@/settings/admin-panel/components/SettingsAdminTabSkeletonLoader';
import { SettingsHealthStatusListCard } from '@/settings/admin-panel/health-status/components/SettingsHealthStatusListCard';
import { SettingsAdminHealthStatusListCard } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthStatusListCard';
import { t } from '@lingui/core/macro';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
@@ -23,7 +23,7 @@ export const SettingsAdminHealthStatus = () => {
title={t`Health Status`}
description={t`How your system is doing`}
/>
<SettingsHealthStatusListCard
<SettingsAdminHealthStatusListCard
services={services}
loading={loadingHealthStatus}
/>
@@ -25,7 +25,7 @@ const HealthStatusIcons: { [k in HealthIndicatorId]: IconComponent } = {
[HealthIndicatorId.app]: IconAppWindow,
};
export const SettingsHealthStatusListCard = ({
export const SettingsAdminHealthStatusListCard = ({
services,
loading,
}: {
@@ -1,6 +1,6 @@
import { ConnectedAccountHealthStatus } from '@/settings/admin-panel/health-status/components/ConnectedAccountHealthStatus';
import { JsonDataIndicatorHealthStatus } from '@/settings/admin-panel/health-status/components/JsonDataIndicatorHealthStatus';
import { WorkerHealthStatus } from '@/settings/admin-panel/health-status/components/WorkerHealthStatus';
import { SettingsAdminConnectedAccountHealthStatus } from '@/settings/admin-panel/health-status/components/SettingsAdminConnectedAccountHealthStatus';
import { SettingsAdminJsonDataIndicatorHealthStatus } from '@/settings/admin-panel/health-status/components/SettingsAdminJsonDataIndicatorHealthStatus';
import { SettingsAdminWorkerHealthStatus } from '@/settings/admin-panel/health-status/components/SettingsAdminWorkerHealthStatus';
import { useParams } from 'react-router-dom';
import { HealthIndicatorId } from '~/generated/graphql';
@@ -11,11 +11,11 @@ export const SettingsAdminIndicatorHealthStatusContent = () => {
case HealthIndicatorId.database:
case HealthIndicatorId.redis:
case HealthIndicatorId.app:
return <JsonDataIndicatorHealthStatus />;
return <SettingsAdminJsonDataIndicatorHealthStatus />;
case HealthIndicatorId.worker:
return <WorkerHealthStatus />;
return <SettingsAdminWorkerHealthStatus />;
case HealthIndicatorId.connectedAccount:
return <ConnectedAccountHealthStatus />;
return <SettingsAdminConnectedAccountHealthStatus />;
default:
return null;
@@ -0,0 +1,140 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { JsonTree } from 'twenty-ui/json-visualizer';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { type QueueJob } from '~/generated-metadata/graphql';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
type SettingsAdminJobDetailsExpandableProps = {
job: QueueJob;
isExpanded: boolean;
};
const StyledDetailsContainer = styled.div`
background-color: ${({ theme }) => theme.background.secondary};
border-top: 1px solid ${({ theme }) => theme.border.color.medium};
padding: ${({ theme }) => theme.spacing(4)};
`;
const StyledSection = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(4)};
&:last-child {
margin-bottom: 0;
}
`;
const StyledSectionTitle = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledPreformattedText = styled.pre`
background-color: ${({ theme }) => theme.background.primary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.danger};
font-family: ${({ theme }) => theme.font.family};
font-size: ${({ theme }) => theme.font.size.sm};
margin: 0;
max-height: 300px;
overflow: auto;
padding: ${({ theme }) => theme.spacing(2)};
white-space: pre-wrap;
word-break: break-word;
`;
const StyledLogEntry = styled.div`
background-color: ${({ theme }) => theme.background.primary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
font-family: ${({ theme }) => theme.font.family};
font-size: ${({ theme }) => theme.font.size.sm};
margin-bottom: ${({ theme }) => theme.spacing(1)};
padding: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsAdminJobDetailsExpandable = ({
job,
isExpanded,
}: SettingsAdminJobDetailsExpandableProps) => {
const { copyToClipboard } = useCopyToClipboard();
const hasData = job.data && Object.keys(job.data).length > 0;
const hasReturnValue =
job.returnValue && Object.keys(job.returnValue).length > 0;
const hasLogs = job.logs && job.logs.length > 0;
const hasStacktrace = job.stackTrace && job.stackTrace.length > 0;
const hasFailedReason = job.failedReason;
const isAnyNode = () => true;
return (
<AnimatedExpandableContainer
isExpanded={isExpanded}
dimension="height"
mode="scroll-height"
>
<StyledDetailsContainer>
{hasFailedReason && (
<StyledSection>
<StyledSectionTitle>{t`Error Message`}</StyledSectionTitle>
<StyledPreformattedText>{job.failedReason}</StyledPreformattedText>
</StyledSection>
)}
{hasStacktrace && job.stackTrace && (
<StyledSection>
<StyledSectionTitle>{t`Stack Trace`}</StyledSectionTitle>
<StyledPreformattedText>
{job.stackTrace.join('\n')}
</StyledPreformattedText>
</StyledSection>
)}
{hasReturnValue && (
<StyledSection>
<StyledSectionTitle>{t`Return Value`}</StyledSectionTitle>
<JsonTree
value={job.returnValue}
shouldExpandNodeInitially={isAnyNode}
emptyArrayLabel={t`Empty Array`}
emptyObjectLabel={t`Empty Object`}
emptyStringLabel={t`[empty string]`}
arrowButtonCollapsedLabel={t`Expand`}
arrowButtonExpandedLabel={t`Collapse`}
onNodeValueClick={copyToClipboard}
/>
</StyledSection>
)}
{hasData && (
<StyledSection>
<StyledSectionTitle>{t`Job Data`}</StyledSectionTitle>
<JsonTree
value={job.data}
shouldExpandNodeInitially={isAnyNode}
emptyArrayLabel={t`Empty Array`}
emptyObjectLabel={t`Empty Object`}
emptyStringLabel={t`[empty string]`}
arrowButtonCollapsedLabel={t`Expand`}
arrowButtonExpandedLabel={t`Collapse`}
onNodeValueClick={copyToClipboard}
/>
</StyledSection>
)}
{hasLogs && (
<StyledSection>
<StyledSectionTitle>{t`Logs`}</StyledSectionTitle>
{job.logs?.map((log, index) => (
<StyledLogEntry key={index}>{log}</StyledLogEntry>
))}
</StyledSection>
)}
</StyledDetailsContainer>
</AnimatedExpandableContainer>
);
};
@@ -0,0 +1,52 @@
import styled from '@emotion/styled';
import { Tag, type TagColor } from 'twenty-ui/components';
import { JobState } from '~/generated-metadata/graphql';
type SettingsAdminJobStateBadgeProps = {
state: JobState;
attemptsMade?: number;
};
const JOB_STATE_COLORS: Record<JobState, TagColor> = {
[JobState.COMPLETED]: 'green',
[JobState.FAILED]: 'red',
[JobState.ACTIVE]: 'blue',
[JobState.WAITING]: 'gray',
[JobState.DELAYED]: 'orange',
[JobState.PRIORITIZED]: 'blue',
[JobState.WAITING_CHILDREN]: 'gray',
};
const StyledContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledAttemptBadge = styled.span`
background-color: ${({ theme }) => theme.background.danger};
border: 1px solid ${({ theme }) => theme.border.color.danger};
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.danger};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.medium};
padding: ${({ theme }) => `${theme.spacing(0.5)} ${theme.spacing(1)}`};
white-space: nowrap;
`;
export const SettingsAdminJobStateBadge = ({
state,
attemptsMade = 1,
}: SettingsAdminJobStateBadgeProps) => {
const color = JOB_STATE_COLORS[state] || 'gray';
const showAttempts = attemptsMade > 1;
return (
<StyledContainer>
<Tag color={color} text={state} />
{showAttempts && (
<StyledAttemptBadge>{attemptsMade} attempts</StyledAttemptBadge>
)}
</StyledContainer>
);
};
@@ -22,7 +22,7 @@ const StyledErrorMessage = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
export const JsonDataIndicatorHealthStatus = () => {
export const SettingsAdminJsonDataIndicatorHealthStatus = () => {
const { t } = useLingui();
const { copyToClipboard } = useCopyToClipboard();
@@ -0,0 +1,69 @@
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { t } from '@lingui/core/macro';
import { IconDotsVertical, IconRefresh, IconTrash } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { JobState } from '~/generated-metadata/graphql';
type SettingsAdminQueueJobRowDropdownMenuProps = {
jobId: string;
jobState: JobState;
onRetry?: () => void;
onDelete: () => void;
};
export const SettingsAdminQueueJobRowDropdownMenu = ({
jobId,
jobState,
onRetry,
onDelete,
}: SettingsAdminQueueJobRowDropdownMenuProps) => {
const dropdownId = `queue-job-row-${jobId}`;
const { closeDropdown } = useCloseDropdown();
const handleRetry = () => {
onRetry?.();
closeDropdown(dropdownId);
};
const handleDelete = () => {
onDelete();
closeDropdown(dropdownId);
};
return (
<Dropdown
dropdownId={dropdownId}
dropdownPlacement="right-start"
clickableComponent={
<LightIconButton
aria-label="Job Actions"
Icon={IconDotsVertical}
accent="tertiary"
/>
}
dropdownComponents={
<DropdownContent>
<DropdownMenuItemsContainer>
{jobState === JobState.FAILED && onRetry && (
<MenuItem
text={t`Retry`}
LeftIcon={IconRefresh}
onClick={handleRetry}
/>
)}
<MenuItem
accent="danger"
text={t`Delete`}
LeftIcon={IconTrash}
onClick={handleDelete}
/>
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
);
};
@@ -0,0 +1,412 @@
import { SettingsAdminDeleteJobsConfirmationModal } from '@/settings/admin-panel/health-status/components/SettingsAdminDeleteJobsConfirmationModal';
import { SettingsAdminJobDetailsExpandable } from '@/settings/admin-panel/health-status/components/SettingsAdminJobDetailsExpandable';
import { SettingsAdminJobStateBadge } from '@/settings/admin-panel/health-status/components/SettingsAdminJobStateBadge';
import { SettingsAdminQueueJobRowDropdownMenu } from '@/settings/admin-panel/health-status/components/SettingsAdminQueueJobRowDropdownMenu';
import { SettingsAdminRetryJobsConfirmationModal } from '@/settings/admin-panel/health-status/components/SettingsAdminRetryJobsConfirmationModal';
import { useDeleteJobs } from '@/settings/admin-panel/health-status/hooks/useDeleteJobs';
import { useRetryJobs } from '@/settings/admin-panel/health-status/hooks/useRetryJobs';
import { Select } from '@/ui/input/components/Select';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { Table } from '@/ui/layout/table/components/Table';
import { TableBody } from '@/ui/layout/table/components/TableBody';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled';
import { plural, t } from '@lingui/core/macro';
import { useState } from 'react';
import { IconRefresh, IconTrash } from 'twenty-ui/display';
import { Button, Checkbox } from 'twenty-ui/input';
import {
JobState,
type QueueJob,
useGetQueueJobsQuery,
} from '~/generated-metadata/graphql';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
type SettingsAdminQueueJobsTableProps = {
queueName: string;
onRetentionConfigLoaded?: (config: {
completedMaxAge: number;
completedMaxCount: number;
failedMaxAge: number;
failedMaxCount: number;
}) => void;
};
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
`;
const StyledControlsContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
justify-content: space-between;
`;
const StyledEmptyState = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
padding: ${({ theme }) => theme.spacing(8)};
text-align: center;
`;
const StyledPaginationContainer = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
padding: ${({ theme }) => theme.spacing(2)};
`;
const StyledTableCell = styled(TableCell)`
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const StyledExpandableTableRow = styled(TableRow)<{ isExpanded: boolean }>`
cursor: pointer;
background-color: ${({ theme, isExpanded }) =>
isExpanded ? theme.background.transparent.light : 'transparent'};
&:hover {
background-color: ${({ theme }) => theme.background.transparent.light};
}
`;
const StyledJobRowWrapper = styled.div`
display: contents;
`;
const StyledCheckboxCell = styled(TableCell)`
justify-content: center;
padding: 0;
padding-left: ${({ theme }) => theme.spacing(1)};
`;
const StyledHeaderCheckboxCell = styled(TableHeader)`
align-items: center;
display: flex;
justify-content: center;
padding-right: ${({ theme }) => theme.spacing(1)};
`;
const StyledButtonGroup = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const RETRY_MODAL_ID = 'retry-jobs-modal';
const DELETE_MODAL_ID = 'delete-jobs-modal';
const LIMIT = 50;
export const SettingsAdminQueueJobsTable = ({
queueName,
onRetentionConfigLoaded,
}: SettingsAdminQueueJobsTableProps) => {
const [page, setPage] = useState(0);
const [stateFilter, setStateFilter] = useState<JobState>(JobState.COMPLETED);
const [expandedJobId, setExpandedJobId] = useState<string | null>(null);
const [selectedJobIds, setSelectedJobIds] = useState<Set<string>>(new Set());
const { openModal } = useModal();
const jobStateOptions: { value: JobState; label: string }[] = [
{ value: JobState.COMPLETED, label: t`Completed` },
{ value: JobState.FAILED, label: t`Failed` },
{ value: JobState.ACTIVE, label: t`Active` },
{ value: JobState.WAITING, label: t`Waiting` },
{ value: JobState.DELAYED, label: t`Delayed` },
{ value: JobState.PRIORITIZED, label: t`Prioritized` },
{ value: JobState.WAITING_CHILDREN, label: t`Waiting Children` },
];
const offset = page * LIMIT;
const { data, loading, refetch } = useGetQueueJobsQuery({
variables: {
queueName,
state: stateFilter,
limit: LIMIT,
offset,
},
fetchPolicy: 'network-only',
});
const { retryJobs, isRetrying } = useRetryJobs(queueName, () => {
refetch();
setSelectedJobIds(new Set());
});
const { deleteJobs, isDeleting } = useDeleteJobs(queueName, () => {
refetch();
setSelectedJobIds(new Set());
});
const jobs = data?.getQueueJobs?.jobs || [];
const hasMore = data?.getQueueJobs?.hasMore || false;
const totalCount = data?.getQueueJobs?.totalCount || 0;
const failedJobs = jobs.filter((job) => job.state === JobState.FAILED);
// Pass retention config to parent when data loads
const shouldPassConfig =
data?.getQueueJobs?.retentionConfig !== undefined &&
onRetentionConfigLoaded !== undefined;
if (shouldPassConfig) {
onRetentionConfigLoaded(data.getQueueJobs.retentionConfig);
}
const selectedCount = selectedJobIds.size;
const allJobsSelected =
jobs.length > 0 && jobs.every((job) => selectedJobIds.has(job.id));
const someJobsSelected =
jobs.some((job) => selectedJobIds.has(job.id)) && !allJobsSelected;
// Check if all selected jobs are failed (for showing retry button)
const selectedJobs = jobs.filter((job) => selectedJobIds.has(job.id));
const allSelectedAreFailed =
selectedJobs.length > 0 &&
selectedJobs.every((job) => job.state === JobState.FAILED);
const handleToggleAll = () => {
const shouldClearSelection = allJobsSelected === true;
if (shouldClearSelection) {
setSelectedJobIds(new Set());
} else {
setSelectedJobIds(new Set(jobs.map((job) => job.id)));
}
};
const handleToggleJob = (event: React.MouseEvent, jobId: string) => {
event.stopPropagation();
const newSelected = new Set(selectedJobIds);
if (newSelected.has(jobId)) {
newSelected.delete(jobId);
} else {
newSelected.add(jobId);
}
setSelectedJobIds(newSelected);
};
const handleRetrySelected = () => {
openModal(RETRY_MODAL_ID);
};
const confirmRetrySelected = async () => {
const jobIdsToRetry = selectedCount > 0 ? Array.from(selectedJobIds) : [];
await retryJobs(jobIdsToRetry);
};
const handleDeleteSelected = () => {
openModal(DELETE_MODAL_ID);
};
const confirmDeleteSelected = async () => {
await deleteJobs(Array.from(selectedJobIds));
};
const handleRetryOne = async (jobId: string) => {
await retryJobs([jobId]);
};
const handleDeleteOne = async (jobId: string) => {
await deleteJobs([jobId]);
};
const handleRowClick = (jobId: string) => {
setExpandedJobId(expandedJobId === jobId ? null : jobId);
};
const formatTimestampRelative = (timestamp?: number | null) => {
if (!timestamp) return '-';
return beautifyPastDateRelativeToNow(timestamp);
};
const formatTimestampFull = (timestamp?: number | null) => {
if (!timestamp) return '';
return new Date(timestamp).toLocaleString();
};
return (
<StyledContainer>
<StyledControlsContainer>
<Select
dropdownId="job-state-filter"
value={stateFilter}
options={jobStateOptions}
onChange={(value) => {
setStateFilter(value as JobState);
setPage(0);
setSelectedJobIds(new Set());
}}
selectSizeVariant="small"
/>
<StyledButtonGroup>
{selectedCount > 0 && (
<Button
Icon={IconTrash}
title={plural(selectedCount, {
one: `Delete ${selectedCount} Job`,
other: `Delete ${selectedCount} Jobs`,
})}
onClick={handleDeleteSelected}
disabled={isDeleting || loading}
size="small"
variant="secondary"
accent="danger"
/>
)}
{allSelectedAreFailed && (
<Button
Icon={IconRefresh}
title={plural(selectedCount, {
one: `Retry ${selectedCount} Job`,
other: `Retry ${selectedCount} Jobs`,
})}
onClick={handleRetrySelected}
disabled={isRetrying || loading}
size="small"
variant="secondary"
/>
)}
{failedJobs.length > 0 && selectedCount === 0 && (
<Button
Icon={IconRefresh}
title={t`Retry All Failed`}
onClick={handleRetrySelected}
disabled={isRetrying || loading}
size="small"
variant="secondary"
/>
)}
</StyledButtonGroup>
</StyledControlsContainer>
{loading && jobs.length === 0 ? (
<StyledEmptyState>{t`Loading jobs...`}</StyledEmptyState>
) : jobs.length === 0 ? (
<StyledEmptyState>{t`No jobs found`}</StyledEmptyState>
) : (
<>
<Table>
<TableRow gridAutoColumns="32px 2fr 1fr 2fr 32px">
<StyledHeaderCheckboxCell>
{jobs.length > 0 && (
<Checkbox
checked={allJobsSelected}
indeterminate={someJobsSelected}
onChange={handleToggleAll}
/>
)}
</StyledHeaderCheckboxCell>
<TableHeader>{t`Job Name`}</TableHeader>
<TableHeader>{t`State`}</TableHeader>
<TableHeader align="right">{t`Timestamp`}</TableHeader>
<TableHeader></TableHeader>
</TableRow>
<TableBody>
{jobs.map((job: QueueJob) => {
const isExpanded = expandedJobId === job.id;
const isSelected = selectedJobIds.has(job.id);
return (
<StyledJobRowWrapper key={job.id}>
<StyledExpandableTableRow
gridAutoColumns="32px 2fr 1fr 2fr 32px"
onClick={() => handleRowClick(job.id)}
isExpanded={isExpanded}
>
<StyledCheckboxCell
onClick={(e) => {
e.stopPropagation();
handleToggleJob(e, job.id);
}}
>
<Checkbox checked={isSelected} />
</StyledCheckboxCell>
<StyledTableCell title={job.name}>
{job.name}
</StyledTableCell>
<TableCell>
<SettingsAdminJobStateBadge
state={job.state}
attemptsMade={job.attemptsMade}
/>
</TableCell>
<TableCell
align="right"
title={formatTimestampFull(
job.finishedOn || job.processedOn || job.timestamp,
)}
>
{formatTimestampRelative(
job.finishedOn || job.processedOn || job.timestamp,
)}
</TableCell>
<TableCell onClick={(e) => e.stopPropagation()}>
<SettingsAdminQueueJobRowDropdownMenu
jobId={job.id}
jobState={job.state}
onRetry={
job.state === JobState.FAILED
? () => handleRetryOne(job.id)
: undefined
}
onDelete={() => handleDeleteOne(job.id)}
/>
</TableCell>
</StyledExpandableTableRow>
<SettingsAdminJobDetailsExpandable
job={job}
isExpanded={isExpanded}
/>
</StyledJobRowWrapper>
);
})}
</TableBody>
</Table>
<StyledPaginationContainer>
<Button
title={t`Previous`}
onClick={() => setPage((p) => Math.max(0, p - 1))}
disabled={page === 0 || loading}
size="small"
variant="secondary"
/>
<div>
{t`Page`} {page + 1} {totalCount > 0 ? t`of` : ''}{' '}
{totalCount > 0 ? Math.max(1, Math.ceil(totalCount / LIMIT)) : ''}
</div>
<Button
title={t`Next`}
onClick={() => setPage((p) => p + 1)}
disabled={!hasMore || loading}
size="small"
variant="secondary"
/>
</StyledPaginationContainer>
</>
)}
<SettingsAdminRetryJobsConfirmationModal
modalId={RETRY_MODAL_ID}
jobCount={selectedCount > 0 ? selectedCount : failedJobs.length}
onConfirm={confirmRetrySelected}
/>
<SettingsAdminDeleteJobsConfirmationModal
modalId={DELETE_MODAL_ID}
jobCount={selectedCount}
onConfirm={confirmDeleteSelected}
/>
</StyledContainer>
);
};
@@ -0,0 +1,38 @@
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { plural, t } from '@lingui/core/macro';
type SettingsAdminRetryJobsConfirmationModalProps = {
modalId: string;
jobCount: number;
onConfirm: () => void;
onClose?: () => void;
};
export const SettingsAdminRetryJobsConfirmationModal = ({
modalId,
jobCount,
onConfirm,
onClose,
}: SettingsAdminRetryJobsConfirmationModalProps) => {
const title = plural(jobCount, {
one: `Retry ${jobCount} Job`,
other: `Retry ${jobCount} Jobs`,
});
const subtitle = plural(jobCount, {
one: `This will retry the selected job. It will be re-executed from the beginning.`,
other: `This will retry the selected jobs. They will be re-executed from the beginning.`,
});
return (
<ConfirmationModal
modalId={modalId}
title={title}
subtitle={subtitle}
onConfirmClick={onConfirm}
onClose={onClose}
confirmButtonText={t`Retry`}
confirmButtonAccent="blue"
/>
);
};
@@ -1,4 +1,4 @@
import { WorkerQueueMetricsSection } from '@/settings/admin-panel/health-status/components/WorkerQueueMetricsSection';
import { SettingsAdminWorkerQueueMetricsSection } from '@/settings/admin-panel/health-status/components/SettingsAdminWorkerQueueMetricsSection';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useContext } from 'react';
@@ -10,7 +10,7 @@ const StyledErrorMessage = styled.div`
margin-top: ${({ theme }) => theme.spacing(2)};
`;
export const WorkerHealthStatus = () => {
export const SettingsAdminWorkerHealthStatus = () => {
const { indicatorHealth } = useContext(SettingsAdminIndicatorHealthContext);
const isWorkerDown =
@@ -25,7 +25,10 @@ export const WorkerHealthStatus = () => {
</StyledErrorMessage>
) : (
(indicatorHealth.queues ?? []).map((queue) => (
<WorkerQueueMetricsSection key={queue.queueName} queue={queue} />
<SettingsAdminWorkerQueueMetricsSection
key={queue.queueName}
queue={queue}
/>
))
)}
</>
@@ -1,5 +1,5 @@
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
import { WorkerMetricsTooltip } from '@/settings/admin-panel/health-status/components/WorkerMetricsTooltip';
import { SettingsAdminWorkerMetricsTooltip } from '@/settings/admin-panel/health-status/components/SettingsAdminWorkerMetricsTooltip';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
@@ -33,16 +33,16 @@ const StyledSettingsAdminTableCard = styled(SettingsAdminTableCard)`
padding-right: ${({ theme }) => theme.spacing(2)};
`;
type WorkerMetricsGraphProps = {
type SettingsAdminWorkerMetricsGraphProps = {
queueName: string;
timeRange: QueueMetricsTimeRange;
onTimeRangeChange: (range: QueueMetricsTimeRange) => void;
};
export const WorkerMetricsGraph = ({
export const SettingsAdminWorkerMetricsGraph = ({
queueName,
timeRange,
}: WorkerMetricsGraphProps) => {
}: SettingsAdminWorkerMetricsGraphProps) => {
const theme = useTheme();
const { enqueueErrorSnackBar } = useSnackBar();
@@ -172,7 +172,9 @@ export const WorkerMetricsGraph = ({
gridYValues={4}
pointSize={0}
enableSlices="x"
sliceTooltip={({ slice }) => <WorkerMetricsTooltip slice={slice} />}
sliceTooltip={({ slice }) => (
<SettingsAdminWorkerMetricsTooltip slice={slice} />
)}
useMesh={true}
legends={[
{
@@ -1,5 +1,5 @@
import styled from '@emotion/styled';
import type { Point, LineSeries } from '@nivo/line';
import type { LineSeries, Point } from '@nivo/line';
import { type ReactElement } from 'react';
const StyledTooltipContainer = styled.div`
@@ -43,15 +43,15 @@ const StyledTooltipValue = styled.span`
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
type WorkerMetricsTooltipProps = {
type SettingsAdminWorkerMetricsTooltipProps = {
slice: {
points: readonly Point<LineSeries>[];
};
};
export const WorkerMetricsTooltip = ({
export const SettingsAdminWorkerMetricsTooltip = ({
slice,
}: WorkerMetricsTooltipProps): ReactElement => {
}: SettingsAdminWorkerMetricsTooltipProps): ReactElement => {
return (
<StyledTooltipContainer>
{slice.points.map((point) => (
@@ -0,0 +1,88 @@
import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader';
import { WORKER_QUEUE_METRICS_SELECT_OPTIONS } from '@/settings/admin-panel/health-status/constants/WorkerQueueMetricsSelectOptions';
import { Select } from '@/ui/input/components/Select';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { lazy, Suspense, useState } from 'react';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { H2Title, IconList } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import {
type AdminPanelWorkerQueueHealth,
QueueMetricsTimeRange,
} from '~/generated-metadata/graphql';
const SettingsAdminWorkerMetricsGraph = lazy(() =>
import('./SettingsAdminWorkerMetricsGraph').then((module) => ({
default: module.SettingsAdminWorkerMetricsGraph,
})),
);
type SettingsAdminWorkerQueueMetricsSectionProps = {
queue: AdminPanelWorkerQueueHealth;
};
const StyledControlsContainer = styled.div`
align-items: flex-start;
display: flex;
justify-content: space-between;
`;
const StyledRightControls = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
margin-bottom: ${({ theme }) => theme.spacing(8)};
`;
export const SettingsAdminWorkerQueueMetricsSection = ({
queue,
}: SettingsAdminWorkerQueueMetricsSectionProps) => {
const [timeRange, setTimeRange] = useState(QueueMetricsTimeRange.OneHour);
return (
<StyledContainer>
<Section>
<StyledControlsContainer>
<H2Title title={queue.queueName} description={t`Queue performance`} />
<StyledRightControls>
<Button
Icon={IconList}
title={t`View Jobs`}
size="small"
variant="secondary"
to={getSettingsPath(SettingsPath.AdminPanelQueueDetail, {
queueName: queue.queueName,
})}
/>
<Select
dropdownId={`timerange-${queue.queueName}`}
value={timeRange}
options={WORKER_QUEUE_METRICS_SELECT_OPTIONS.map((option) => ({
...option,
label: t(option.label),
}))}
onChange={setTimeRange}
needIconCheck
selectSizeVariant="small"
/>
</StyledRightControls>
</StyledControlsContainer>
</Section>
<Suspense fallback={<ChartSkeletonLoader />}>
<SettingsAdminWorkerMetricsGraph
queueName={queue.queueName}
timeRange={timeRange}
onTimeRangeChange={setTimeRange}
/>
</Suspense>
</StyledContainer>
);
};
@@ -1,68 +0,0 @@
import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader';
import { WORKER_QUEUE_METRICS_SELECT_OPTIONS } from '@/settings/admin-panel/health-status/constants/WorkerQueueMetricsSelectOptions';
import { Select } from '@/ui/input/components/Select';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { lazy, Suspense, useState } from 'react';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import {
type AdminPanelWorkerQueueHealth,
QueueMetricsTimeRange,
} from '~/generated-metadata/graphql';
const WorkerMetricsGraph = lazy(() =>
import('./WorkerMetricsGraph').then((module) => ({
default: module.WorkerMetricsGraph,
})),
);
type WorkerQueueMetricsSectionProps = {
queue: AdminPanelWorkerQueueHealth;
};
const StyledControlsContainer = styled.div`
display: flex;
align-items: flex-start;
justify-content: space-between;
`;
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
margin-bottom: ${({ theme }) => theme.spacing(8)};
`;
export const WorkerQueueMetricsSection = ({
queue,
}: WorkerQueueMetricsSectionProps) => {
const [timeRange, setTimeRange] = useState(QueueMetricsTimeRange.OneHour);
return (
<StyledContainer>
<Section>
<StyledControlsContainer>
<H2Title title={queue.queueName} description={t`Queue performance`} />
<Select
dropdownId={`timerange-${queue.queueName}`}
value={timeRange}
options={WORKER_QUEUE_METRICS_SELECT_OPTIONS.map((option) => ({
...option,
label: t(option.label),
}))}
onChange={setTimeRange}
needIconCheck
selectSizeVariant="small"
/>
</StyledControlsContainer>
</Section>
<Suspense fallback={<ChartSkeletonLoader />}>
<WorkerMetricsGraph
queueName={queue.queueName}
timeRange={timeRange}
onTimeRangeChange={setTimeRange}
/>
</Suspense>
</StyledContainer>
);
};
@@ -0,0 +1,14 @@
import { gql } from '@apollo/client';
export const DELETE_JOBS = gql`
mutation DeleteJobs($queueName: String!, $jobIds: [String!]!) {
deleteJobs(queueName: $queueName, jobIds: $jobIds) {
deletedCount
results {
jobId
success
error
}
}
}
`;
@@ -0,0 +1,14 @@
import { gql } from '@apollo/client';
export const RETRY_JOBS = gql`
mutation RetryJobs($queueName: String!, $jobIds: [String!]!) {
retryJobs(queueName: $queueName, jobIds: $jobIds) {
retriedCount
results {
jobId
success
error
}
}
}
`;
@@ -0,0 +1,41 @@
import { gql } from '@apollo/client';
export const GET_QUEUE_JOBS = gql`
query GetQueueJobs(
$queueName: String!
$state: JobState!
$limit: Int
$offset: Int
) {
getQueueJobs(
queueName: $queueName
state: $state
limit: $limit
offset: $offset
) {
jobs {
id
name
data
state
timestamp
failedReason
processedOn
finishedOn
attemptsMade
returnValue
logs
stackTrace
}
count
totalCount
hasMore
retentionConfig {
completedMaxAge
completedMaxCount
failedMaxAge
failedMaxCount
}
}
}
`;
@@ -0,0 +1,83 @@
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { plural, t } from '@lingui/core/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useDeleteJobsMutation } from '~/generated-metadata/graphql';
import { getErrorMessageFromApolloError } from '~/utils/get-error-message-from-apollo-error.util';
export const useDeleteJobs = (queueName: string, onSuccess?: () => void) => {
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [isDeleting, setIsDeleting] = useState(false);
const [deleteJobsMutation] = useDeleteJobsMutation();
const deleteJobs = async (jobIds: string[]) => {
setIsDeleting(true);
try {
const result = await deleteJobsMutation({
variables: {
queueName,
jobIds,
},
});
const response = result.data?.deleteJobs;
if (isDefined(response)) {
const { deletedCount, results } = response;
const failedResults = results.filter((r) => !r.success);
if (deletedCount > 0) {
if (failedResults.length > 0) {
enqueueSuccessSnackBar({
message: plural(deletedCount, {
one: `Successfully deleted ${deletedCount} job`,
other: `Successfully deleted ${deletedCount} jobs`,
}),
});
enqueueErrorSnackBar({
message: plural(failedResults.length, {
one: `${failedResults.length} job could not be deleted`,
other: `${failedResults.length} jobs could not be deleted`,
}),
});
} else {
enqueueSuccessSnackBar({
message: plural(deletedCount, {
one: `Successfully deleted ${deletedCount} job`,
other: `Successfully deleted ${deletedCount} jobs`,
}),
});
}
onSuccess?.();
} else {
const errorMessages = failedResults
.map((r) => r.error)
.filter(Boolean);
const errorDetails =
errorMessages.length > 0 ? `: ${errorMessages[0]}` : '';
enqueueErrorSnackBar({
message: t`No jobs were deleted${errorDetails}`,
});
}
}
} catch (error) {
enqueueErrorSnackBar({
message:
error instanceof ApolloError
? getErrorMessageFromApolloError(error)
: t`Failed to delete jobs. Please try again later.`,
});
} finally {
setIsDeleting(false);
}
};
return {
deleteJobs,
isDeleting,
};
};
@@ -0,0 +1,87 @@
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { plural, t } from '@lingui/core/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useRetryJobsMutation } from '~/generated-metadata/graphql';
import { getErrorMessageFromApolloError } from '~/utils/get-error-message-from-apollo-error.util';
export const useRetryJobs = (queueName: string, onSuccess?: () => void) => {
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [isRetrying, setIsRetrying] = useState(false);
const [retryJobsMutation] = useRetryJobsMutation();
const retryJobs = async (jobIds: string[]) => {
setIsRetrying(true);
try {
const result = await retryJobsMutation({
variables: {
queueName,
jobIds,
},
});
const response = result.data?.retryJobs;
if (isDefined(response)) {
const { retriedCount, results } = response;
const failedResults = results.filter((r) => !r.success);
if (retriedCount === -1) {
enqueueSuccessSnackBar({
message: t`All failed jobs have been retried`,
});
} else if (retriedCount > 0) {
if (failedResults.length > 0) {
enqueueSuccessSnackBar({
message: plural(retriedCount, {
one: `Successfully retried ${retriedCount} job`,
other: `Successfully retried ${retriedCount} jobs`,
}),
});
enqueueErrorSnackBar({
message: plural(failedResults.length, {
one: `${failedResults.length} job could not be retried`,
other: `${failedResults.length} jobs could not be retried`,
}),
});
} else {
enqueueSuccessSnackBar({
message: plural(retriedCount, {
one: `Successfully retried ${retriedCount} job`,
other: `Successfully retried ${retriedCount} jobs`,
}),
});
}
} else {
const errorMessages = failedResults
.map((r) => r.error)
.filter(Boolean);
const errorDetails =
errorMessages.length > 0 ? `: ${errorMessages[0]}` : '';
enqueueErrorSnackBar({
message: t`No jobs were retried${errorDetails}`,
});
}
onSuccess?.();
}
} catch (error) {
enqueueErrorSnackBar({
message:
error instanceof ApolloError
? getErrorMessageFromApolloError(error)
: t`Failed to retry jobs. Please try again later.`,
});
} finally {
setIsRetrying(false);
}
};
return {
retryJobs,
isRetrying,
};
};
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { plural } from '@lingui/core/macro';
import { useMemo, useRef, useState, type MouseEvent } from 'react';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
@@ -166,7 +167,14 @@ export const SettingsMorphRelationMultiSelect = ({
<MultiSelectControl
selectedOptions={selectedOptions}
fixedIcon={selectedOptions.length < 2 ? undefined : IconBox}
fixedText={selectedOptions.length < 2 ? undefined : 'Object'}
fixedText={
selectedOptions.length < 2
? undefined
: plural(selectedOptions.length, {
one: `# Object`,
other: `# Objects`,
})
}
isDisabled={isDisabled}
selectSizeVariant={selectSizeVariant}
hasRightElement={hasRightElement}
@@ -181,7 +189,14 @@ export const SettingsMorphRelationMultiSelect = ({
<MultiSelectControl
selectedOptions={selectedOptions}
fixedIcon={selectedOptions.length < 2 ? undefined : IconBox}
fixedText={selectedOptions.length < 2 ? undefined : 'Object'}
fixedText={
selectedOptions.length < 2
? undefined
: plural(selectedOptions.length, {
one: `# Object`,
other: `# Objects`,
})
}
isDisabled={isDisabled}
selectSizeVariant={selectSizeVariant}
hasRightElement={hasRightElement}
@@ -8,6 +8,7 @@ import { SettingsOptionCardContentCounter } from '@/settings/components/Settings
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
import { NUMBER_DATA_MODEL_SELECT_OPTIONS } from '@/settings/data-model/fields/forms/number/constants/NumberDataModelSelectOptions';
import { Select } from '@/ui/input/components/Select';
import { plural } from '@lingui/core/macro';
import { useLingui } from '@lingui/react/macro';
import { IconDecimal, IconEye } from 'twenty-ui/display';
import { DEFAULT_DECIMAL_VALUE } from '~/utils/format/formatNumber';
@@ -81,7 +82,10 @@ export const SettingsDataModelFieldNumberForm = ({
<SettingsOptionCardContentCounter
Icon={IconDecimal}
title={t`Number of decimals`}
description={`E.g. ${(type === 'percentage' ? 99 : 1000).toFixed(count)}${type === 'percentage' ? '%' : ''} for ${count} decimal${count > 1 ? 's' : ''}`}
description={plural(count, {
one: `E.g. ${(type === 'percentage' ? 99 : 1000).toFixed(count)}${type === 'percentage' ? '%' : ''} for ${count} decimal`,
other: `E.g. ${(type === 'percentage' ? 99 : 1000).toFixed(count)}${type === 'percentage' ? '%' : ''} for ${count} decimals`,
})}
value={count}
onChange={(value) => onChange({ type: type, decimals: value })}
disabled={disabled}
@@ -4,8 +4,7 @@ import { type SettingsRolePermissionsObjectPermission } from '@/settings/roles/r
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import pluralize from 'pluralize';
import { plural } from '@lingui/core/macro';
import { Checkbox, CheckboxAccent } from 'twenty-ui/input';
const StyledPermissionCell = styled(TableCell)`
@@ -59,8 +58,6 @@ export const SettingsRolePermissionsObjectsTableRow = ({
const isRevoked =
revokedBy !== undefined && revokedBy !== null && revokedBy > 0;
const label = permission.label;
const pluralizedRevokedObject = pluralize('object', revokedBy);
const pluralizedGrantedObject = pluralize('object', grantedBy);
const isDisabled = !isEditable;
const handleRowClick = () => {
@@ -83,12 +80,18 @@ export const SettingsRolePermissionsObjectsTableRow = ({
{isRevoked && revokedBy > 0 ? (
<>
{' · '}
{t`Revoked for ${revokedBy} ${pluralizedRevokedObject}`}
{plural(revokedBy, {
one: `Revoked for ${revokedBy} object`,
other: `Revoked for ${revokedBy} objects`,
})}
</>
) : grantedBy && grantedBy > 0 ? (
<>
{' · '}
{t`Granted for ${grantedBy} ${pluralizedGrantedObject}`}
{plural(grantedBy, {
one: `Granted for ${grantedBy} object`,
other: `Granted for ${grantedBy} objects`,
})}
</>
) : null}
</StyledOverrideInfo>
@@ -4,7 +4,6 @@ import {
StyledSelectControlIconChevronDown,
} from '@/ui/input/components/SelectControl';
import { useTheme } from '@emotion/react';
import pluralize from 'pluralize';
import React from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
@@ -39,7 +38,6 @@ export const MultiSelectControl = ({
}
const firstSelectedOption = selectedOptions[0];
const selectedOptionsCount = selectedOptions.length;
return (
<StyledControlContainer
disabled={isDisabled}
@@ -62,11 +60,7 @@ export const MultiSelectControl = ({
/>
) : null}
{isDefined(fixedText) ? (
<OverflowingTextWithTooltip
text={`${selectedOptionsCount} ${
selectedOptionsCount <= 1 ? fixedText : pluralize(fixedText)
}`}
/>
<OverflowingTextWithTooltip text={fixedText} />
) : (
<OverflowingTextWithTooltip text={firstSelectedOption.label} />
)}
@@ -13,7 +13,7 @@ import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId';
import { plural } from 'pluralize';
import { plural } from '@lingui/core/macro';
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconFilter } from 'twenty-ui/display';
@@ -68,8 +68,10 @@ export const AdvancedFilterChip = () => {
const advancedFilterCount = childRecordFiltersAndRecordFilterGroups.length;
const labelText = 'advanced rule';
const chipLabel = `${advancedFilterCount} ${advancedFilterCount === 1 ? labelText : plural(labelText)}`;
const chipLabel = plural(advancedFilterCount, {
one: `${advancedFilterCount} advanced rule`,
other: `${advancedFilterCount} advanced rules`,
});
const { objectMetadataItems } = useObjectMetadataItems();
@@ -0,0 +1,88 @@
import { SettingsAdminQueueJobsTable } from '@/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { plural, t } from '@lingui/core/macro';
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
export const SettingsAdminQueueDetail = () => {
const { queueName } = useParams<{ queueName: string }>();
const [retentionConfig, setRetentionConfig] = useState<{
completedMaxAge: number;
completedMaxCount: number;
failedMaxAge: number;
failedMaxCount: number;
} | null>(null);
if (!queueName) {
return null;
}
const formatDuration = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const days = Math.floor(seconds / 86400);
if (days >= 1) {
return plural(days, {
one: `${days} day`,
other: `${days} days`,
});
}
return plural(hours, {
one: `${hours} hour`,
other: `${hours} hours`,
});
};
const completedDuration = retentionConfig
? formatDuration(retentionConfig.completedMaxAge)
: '';
const failedDuration = retentionConfig
? formatDuration(retentionConfig.failedMaxAge)
: '';
const maxCount = retentionConfig ? retentionConfig.completedMaxCount : 0;
const queueDescription = retentionConfig
? t`Completed jobs kept for ${completedDuration}, failed jobs kept for ${failedDuration} (max ${maxCount} each)`
: t`Loading retention configuration...`;
return (
<SubMenuTopBarContainer
title={t`Queue: ${queueName}`}
links={[
{
children: t`Admin Panel`,
href: getSettingsPath(SettingsPath.AdminPanel),
},
{
children: t`Health Status`,
href: getSettingsPath(SettingsPath.AdminPanelHealthStatus),
},
{
children: t`Worker`,
href: getSettingsPath(SettingsPath.AdminPanelIndicatorHealthStatus, {
indicatorId: 'worker',
}),
},
{
children: queueName,
},
]}
>
<SettingsPageContainer>
<Section>
<H2Title title={t`Jobs`} description={queueDescription} />
<SettingsAdminQueueJobsTable
queueName={queueName}
onRetentionConfigLoaded={setRetentionConfig}
/>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};
@@ -17,7 +17,7 @@ import {
} from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import { useRemoveAgentHandoffMutation } from '~/generated-metadata/graphql';
import { type AgentHandoffDto } from '~/generated/graphql';
import { type AgentHandoff } from '~/generated/graphql';
import { normalizeSearchText } from '~/utils/normalizeSearchText';
const AGENT_HANDOFF_DELETION_MODAL_ID = 'agent-handoff-deletion-modal';
@@ -51,7 +51,7 @@ const StyledTableCell = styled(TableCell)`
type SettingsAgentHandoffTableProps = {
agentId: string;
handoffTargets: AgentHandoffDto[];
handoffTargets: AgentHandoff[];
onHandoffRemoved: () => void;
};
@@ -19,14 +19,14 @@ export const mockBillingPlans = {
},
prices: [
{
__typename: 'BillingPriceLicensedDTO',
__typename: 'BillingPriceLicensed',
stripePriceId: 'price_1RyF4BQh1LFc4XrXvt7DwNjS',
unitAmount: 1200,
recurringInterval: 'Month',
priceUsageType: 'LICENSED',
},
{
__typename: 'BillingPriceLicensedDTO',
__typename: 'BillingPriceLicensed',
stripePriceId: 'price_1RyF4BQh1LFc4XrXK0QSeC7f',
unitAmount: 10800,
recurringInterval: 'Year',
@@ -49,19 +49,19 @@ export const mockBillingPlans = {
},
prices: [
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Year',
stripePriceId: 'price_1S5247Qh1LFc4XrXz7p0wdAc',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 999000,
unitAmount: null,
upTo: 7500000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -69,19 +69,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Year',
stripePriceId: 'price_1S5246Qh1LFc4XrX2SdtgqXV',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 399000,
unitAmount: null,
upTo: 2600000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -89,19 +89,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Year',
stripePriceId: 'price_1S5246Qh1LFc4XrXs78BMPQ9',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 199000,
unitAmount: null,
upTo: 1200000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -109,19 +109,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Year',
stripePriceId: 'price_1S5246Qh1LFc4XrXyMEu78zC',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 99000,
unitAmount: null,
upTo: 540000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -129,19 +129,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Year',
stripePriceId: 'price_1S5245Qh1LFc4XrXcNX3ZHn2',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 29000,
unitAmount: null,
upTo: 130000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -149,19 +149,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Year',
stripePriceId: 'price_1S5245Qh1LFc4XrXBSZDK2pI',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 0,
unitAmount: null,
upTo: 50000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -169,19 +169,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Month',
stripePriceId: 'price_1S5245Qh1LFc4XrXPCSjYIq0',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 99900,
unitAmount: null,
upTo: 700000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -189,19 +189,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Month',
stripePriceId: 'price_1S5244Qh1LFc4XrXswQAQfQh',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 39900,
unitAmount: null,
upTo: 240000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -209,19 +209,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Month',
stripePriceId: 'price_1S5244Qh1LFc4XrXB8yPMpce',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 19900,
unitAmount: null,
upTo: 110000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -229,19 +229,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Month',
stripePriceId: 'price_1S5244Qh1LFc4XrXa5wYhu1M',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 9900,
unitAmount: null,
upTo: 50000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -249,19 +249,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Month',
stripePriceId: 'price_1S5243Qh1LFc4XrX1rtoEZD3',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 2900,
unitAmount: null,
upTo: 10000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -269,19 +269,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Month',
stripePriceId: 'price_1S5243Qh1LFc4XrXaeDkjNQq',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 0,
unitAmount: null,
upTo: 5000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -309,14 +309,14 @@ export const mockBillingPlans = {
},
prices: [
{
__typename: 'BillingPriceLicensedDTO',
__typename: 'BillingPriceLicensed',
stripePriceId: 'price_1RyF49Qh1LFc4XrX2W6yGRpc',
unitAmount: 2500,
recurringInterval: 'Month',
priceUsageType: 'LICENSED',
},
{
__typename: 'BillingPriceLicensedDTO',
__typename: 'BillingPriceLicensed',
stripePriceId: 'price_1RyF48Qh1LFc4XrXw5Tr704k',
unitAmount: 22800,
recurringInterval: 'Year',
@@ -339,19 +339,19 @@ export const mockBillingPlans = {
},
prices: [
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Year',
stripePriceId: 'price_1S4GiuQh1LFc4XrX9IZ68tqy',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 999000,
unitAmount: null,
upTo: 7500000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -359,19 +359,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Year',
stripePriceId: 'price_1S4GirQh1LFc4XrXJxV0yJEX',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 399000,
unitAmount: null,
upTo: 2600000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -379,19 +379,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Year',
stripePriceId: 'price_1S4GirQh1LFc4XrX6fK2qrQm',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 199000,
unitAmount: null,
upTo: 1200000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -399,19 +399,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Year',
stripePriceId: 'price_1S4GipQh1LFc4XrXeamfdifM',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 99000,
unitAmount: null,
upTo: 540000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -419,19 +419,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Year',
stripePriceId: 'price_1S4GipQh1LFc4XrXKxrUDfnE',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 29000,
unitAmount: null,
upTo: 130000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -439,19 +439,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Year',
stripePriceId: 'price_1S4GioQh1LFc4XrXMTbLurFo',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 0,
unitAmount: null,
upTo: 50000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -459,19 +459,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Month',
stripePriceId: 'price_1S4GinQh1LFc4XrXP3sgt8Cn',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 99900,
unitAmount: null,
upTo: 700000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -479,19 +479,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Month',
stripePriceId: 'price_1S4GigQh1LFc4XrXGsuGslfk',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 39900,
unitAmount: null,
upTo: 240000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -499,19 +499,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Month',
stripePriceId: 'price_1S4GifQh1LFc4XrXCsT2PvH2',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 19900,
unitAmount: null,
upTo: 110000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -519,19 +519,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Month',
stripePriceId: 'price_1S4GifQh1LFc4XrXZ0ZIrjeM',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 9900,
unitAmount: null,
upTo: 50000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -539,19 +539,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Month',
stripePriceId: 'price_1S4GieQh1LFc4XrX8njwswjQ',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 2900,
unitAmount: null,
upTo: 10000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -559,19 +559,19 @@ export const mockBillingPlans = {
],
},
{
__typename: 'BillingPriceMeteredDTO',
__typename: 'BillingPriceMetered',
priceUsageType: 'METERED',
recurringInterval: 'Month',
stripePriceId: 'price_1S4GieQh1LFc4XrXDpVbLuuE',
tiers: [
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: 0,
unitAmount: null,
upTo: 5000000,
},
{
__typename: 'BillingPriceTierDTO',
__typename: 'BillingPriceTier',
flatAmount: null,
unitAmount: null,
upTo: null,
@@ -23,6 +23,7 @@ describe('ClickHouseService', () => {
let service: ClickHouseService;
let twentyConfigService: TwentyConfigService;
let mockClickHouseClient: jest.Mocked<ClickHouseClient>;
let loggerErrorSpy: jest.SpyInstance;
beforeEach(async () => {
jest.clearAllMocks();
@@ -56,6 +57,11 @@ describe('ClickHouseService', () => {
service = module.get<ClickHouseService>(ClickHouseService);
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
// Mock logger error method to avoid polluting test output
loggerErrorSpy = jest
.spyOn((service as any).logger, 'error')
.mockImplementation();
// Set the mock client
(service as any).mainClient = mockClickHouseClient;
});
@@ -119,8 +125,10 @@ describe('ClickHouseService', () => {
const result = await service.insert('test_table', testData);
expect(result).toEqual({ success: false });
// Since the service uses logger.error instead of exceptionHandlerService.captureExceptions,
// we don't need to assert on exceptionHandlerService
expect(loggerErrorSpy).toHaveBeenCalledWith(
'Error inserting data into ClickHouse',
testError,
);
});
});
@@ -161,8 +169,10 @@ describe('ClickHouseService', () => {
const result = await service.select(query);
expect(result).toEqual([]);
// Since the service uses logger.error instead of exceptionHandlerService.captureExceptions,
// we don't need to assert on exceptionHandlerService
expect(loggerErrorSpy).toHaveBeenCalledWith(
'Error executing select query in ClickHouse',
testError,
);
});
});
@@ -129,7 +129,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
(await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
CommonQueryNames.createMany,
CommonQueryNames.CREATE_MANY,
args,
//TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency
)) as CreateManyQueryArgs;
@@ -205,7 +205,7 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
const enrichedRecords = await this.enrichResultsWithGettersAndHooks({
results: objectRecords,
operationName: CommonQueryNames.findMany,
operationName: CommonQueryNames.FIND_MANY,
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
@@ -272,7 +272,7 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
(await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
CommonQueryNames.findMany,
CommonQueryNames.FIND_MANY,
args,
//TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency
)) as FindManyQueryArgs;
@@ -138,7 +138,7 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
operationName: CommonQueryNames.findOne,
operationName: CommonQueryNames.FIND_ONE,
});
return enrichedResults[0];
@@ -157,7 +157,7 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
(await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
CommonQueryNames.findOne,
CommonQueryNames.FIND_ONE,
args,
//TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency
)) as FindOneQueryArgs;
@@ -311,7 +311,7 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
(await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
CommonQueryNames.groupBy,
CommonQueryNames.GROUP_BY,
args,
//TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency
)) as GroupByQueryArgs;
@@ -12,10 +12,10 @@ import {
import { type CommonSelectedFields } from 'src/engine/api/common/types/common-selected-fields-result.type';
export enum CommonQueryNames {
findOne = 'findOne',
findMany = 'findMany',
createMany = 'createMany',
groupBy = 'groupBy',
FIND_ONE = 'findOne',
FIND_MANY = 'findMany',
CREATE_MANY = 'createMany',
GROUP_BY = 'groupBy',
}
export interface FindOneQueryArgs {
@@ -2,11 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import graphqlFields from 'graphql-fields';
import { type ObjectRecord } from 'twenty-shared/types';
import {
assertIsDefinedOrThrow,
capitalize,
isDefined,
} from 'twenty-shared/utils';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { type ObjectLiteral } from 'typeorm';
import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
@@ -14,7 +10,6 @@ import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interf
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import {
type ResolverArgs,
ResolverArgsType,
type WorkspaceResolverBuilderMethodNames,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -115,8 +110,7 @@ export abstract class GraphqlQueryBaseResolverService<
const computedArgs = (await this.queryRunnerArgsFactory.create(
hookedArgs,
options,
// @ts-expect-error legacy noImplicitAny
ResolverArgsType[capitalize(operationName)],
operationName,
)) as Input;
let roleId: string | undefined;
@@ -89,7 +89,7 @@ describe('QueryRunnerArgsFactory', () => {
const result = await factory.create(
args,
options,
ResolverArgsType.CreateMany,
ResolverArgsType.CREATE_MANY,
);
expect(result).toEqual(args);
@@ -104,7 +104,7 @@ describe('QueryRunnerArgsFactory', () => {
const result = await factory.create(
args,
options,
ResolverArgsType.CreateMany,
ResolverArgsType.CREATE_MANY,
);
const expectedArgs = {
@@ -140,7 +140,7 @@ describe('QueryRunnerArgsFactory', () => {
const result = await factory.create(
args,
options,
ResolverArgsType.CreateMany,
ResolverArgsType.CREATE_MANY,
);
const expectedArgs = {
@@ -176,7 +176,7 @@ describe('QueryRunnerArgsFactory', () => {
const result = await factory.create(
args,
options,
ResolverArgsType.FindMany,
ResolverArgsType.FIND_MANY,
);
expect(result).toEqual({
@@ -194,7 +194,7 @@ describe('QueryRunnerArgsFactory', () => {
const result = await factory.create(
args,
options,
ResolverArgsType.FindOne,
ResolverArgsType.FIND_ONE,
);
expect(result).toEqual({
@@ -212,7 +212,7 @@ describe('QueryRunnerArgsFactory', () => {
const result = await factory.create(
args,
options,
ResolverArgsType.FindDuplicates,
ResolverArgsType.FIND_DUPLICATES,
);
expect(result).toEqual({
@@ -17,6 +17,7 @@ import {
ResolverArgsType,
type UpdateManyResolverArgs,
type UpdateOneResolverArgs,
type WorkspaceResolverBuilderMethodNames,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@@ -36,7 +37,7 @@ export class QueryRunnerArgsFactory {
async create(
args: ResolverArgs,
options: WorkspaceQueryRunnerOptions,
resolverArgsType: ResolverArgsType,
resolverArgsType: WorkspaceResolverBuilderMethodNames,
) {
const fieldMetadataMapByNameByName =
options.objectMetadataItemWithFieldMaps.fieldsById;
@@ -44,7 +45,7 @@ export class QueryRunnerArgsFactory {
const { objectMetadataItemWithFieldMaps, authContext } = options;
switch (resolverArgsType) {
case ResolverArgsType.CreateOne:
case ResolverArgsType.CREATE_ONE:
return {
...args,
data: (
@@ -55,7 +56,7 @@ export class QueryRunnerArgsFactory {
})
)[0],
} satisfies CreateOneResolverArgs;
case ResolverArgsType.CreateMany:
case ResolverArgsType.CREATE_MANY:
return {
...args,
data: await this.overrideDataByFieldMetadata({
@@ -64,7 +65,7 @@ export class QueryRunnerArgsFactory {
objectMetadataItemWithFieldMaps,
}),
} satisfies CreateManyResolverArgs;
case ResolverArgsType.UpdateOne:
case ResolverArgsType.UPDATE_ONE:
return {
...args,
id: (args as UpdateOneResolverArgs).id,
@@ -77,7 +78,7 @@ export class QueryRunnerArgsFactory {
})
)[0],
} satisfies UpdateOneResolverArgs;
case ResolverArgsType.UpdateMany:
case ResolverArgsType.UPDATE_MANY:
return {
...args,
filter: this.overrideFilterByFieldMetadata(
@@ -93,7 +94,7 @@ export class QueryRunnerArgsFactory {
})
)[0],
} satisfies UpdateManyResolverArgs;
case ResolverArgsType.FindOne:
case ResolverArgsType.FIND_ONE:
return {
...args,
filter: this.overrideFilterByFieldMetadata(
@@ -101,7 +102,7 @@ export class QueryRunnerArgsFactory {
options.objectMetadataItemWithFieldMaps,
),
};
case ResolverArgsType.FindMany:
case ResolverArgsType.FIND_MANY:
return {
...args,
filter: this.overrideFilterByFieldMetadata(
@@ -109,7 +110,7 @@ export class QueryRunnerArgsFactory {
options.objectMetadataItemWithFieldMaps,
),
};
case ResolverArgsType.FindDuplicates:
case ResolverArgsType.FIND_DUPLICATES:
return {
...args,
ids: (await Promise.all(
@@ -129,7 +130,7 @@ export class QueryRunnerArgsFactory {
shouldBackfillPositionIfUndefined: false,
}),
} satisfies FindDuplicatesResolverArgs;
case ResolverArgsType.MergeMany:
case ResolverArgsType.MERGE_MANY:
return {
...args,
ids: (await Promise.all(
@@ -146,7 +147,7 @@ export class QueryRunnerArgsFactory {
.conflictPriorityIndex,
dryRun: (args as MergeManyResolverArgs).dryRun,
} satisfies MergeManyResolverArgs;
case ResolverArgsType.GroupBy:
case ResolverArgsType.GROUP_BY:
return {
...args,
filter: this.overrideFilterByFieldMetadata(
@@ -10,27 +10,15 @@ import {
type ObjectRecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
import { type workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Resolver<Args = any> = GraphQLFieldResolver<any, any, Args>;
export enum ResolverArgsType {
FindMany = 'FindMany',
FindOne = 'FindOne',
FindDuplicates = 'FindDuplicates',
CreateOne = 'CreateOne',
CreateMany = 'CreateMany',
UpdateOne = 'UpdateOne',
UpdateMany = 'UpdateMany',
DeleteOne = 'DeleteOne',
DeleteMany = 'DeleteMany',
RestoreMany = 'RestoreMany',
DestroyMany = 'DestroyMany',
DestroyOne = 'DestroyOne',
MergeMany = 'MergeMany',
GroupBy = 'GroupBy',
}
// Use RESOLVER_METHOD_NAMES as the single source of truth for operation names
// This avoids duplication and ensures consistency across the codebase
export const ResolverArgsType = RESOLVER_METHOD_NAMES;
export interface FindManyResolverArgs<
Filter extends ObjectRecordFilter = ObjectRecordFilter,
@@ -5,7 +5,7 @@ import { type Redis } from 'ioredis';
import { AdminPanelHealthService } from 'src/engine/core-modules/admin-panel/admin-panel-health.service';
import { HEALTH_INDICATORS } from 'src/engine/core-modules/admin-panel/constants/health-indicators.constants';
import { type SystemHealth } from 'src/engine/core-modules/admin-panel/dtos/system-health.dto';
import { type SystemHealthDTO } from 'src/engine/core-modules/admin-panel/dtos/system-health.dto';
import { AdminPanelHealthServiceStatus } from 'src/engine/core-modules/admin-panel/enums/admin-panel-health-service-status.enum';
import { QueueMetricsTimeRange } from 'src/engine/core-modules/admin-panel/enums/queue-metrics-time-range.enum';
import { HEALTH_ERROR_MESSAGES } from 'src/engine/core-modules/health/constants/health-error-messages.constants';
@@ -40,6 +40,7 @@ describe('AdminPanelHealthService', () => {
appHealth = { isHealthy: jest.fn() } as any;
redisClient = {
getClient: jest.fn().mockReturnValue({} as Redis),
getQueueClient: jest.fn().mockReturnValue({} as Redis),
} as any;
twentyConfigService = { get: jest.fn() } as any;
@@ -150,7 +151,7 @@ describe('AdminPanelHealthService', () => {
const result = await service.getSystemHealthStatus();
const expected: SystemHealth = {
const expected: SystemHealthDTO = {
services: [
{
...HEALTH_INDICATORS[HealthIndicatorId.database],
@@ -372,6 +373,7 @@ describe('AdminPanelHealthService', () => {
beforeEach(() => {
jest.clearAllMocks();
redisClient.getClient.mockReturnValue({} as Redis);
redisClient.getQueueClient.mockReturnValue({} as Redis);
(Queue as unknown as jest.Mock).mockImplementation(() => mockQueue);
});
@@ -7,9 +7,9 @@ import {
import { Queue } from 'bullmq';
import { HEALTH_INDICATORS } from 'src/engine/core-modules/admin-panel/constants/health-indicators.constants';
import { type AdminPanelHealthServiceData } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-health-service-data.dto';
import { type QueueMetricsData } from 'src/engine/core-modules/admin-panel/dtos/queue-metrics-data.dto';
import { type SystemHealth } from 'src/engine/core-modules/admin-panel/dtos/system-health.dto';
import { type AdminPanelHealthServiceDataDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-health-service-data.dto';
import { type QueueMetricsDataDTO } from 'src/engine/core-modules/admin-panel/dtos/queue-metrics-data.dto';
import { type SystemHealthDTO } from 'src/engine/core-modules/admin-panel/dtos/system-health.dto';
import { AdminPanelHealthServiceStatus } from 'src/engine/core-modules/admin-panel/enums/admin-panel-health-service-status.enum';
import { QueueMetricsTimeRange } from 'src/engine/core-modules/admin-panel/enums/queue-metrics-time-range.enum';
import { HealthIndicatorId } from 'src/engine/core-modules/health/enums/health-indicator-id.enum';
@@ -117,7 +117,7 @@ export class AdminPanelHealthService {
async getIndicatorHealthStatus(
indicatorId: HealthIndicatorId,
): Promise<AdminPanelHealthServiceData> {
): Promise<AdminPanelHealthServiceDataDTO> {
const healthIndicator = this.healthIndicators[indicatorId];
if (!healthIndicator) {
@@ -145,7 +145,7 @@ export class AdminPanelHealthService {
return indicatorStatus;
}
async getSystemHealthStatus(): Promise<SystemHealth> {
async getSystemHealthStatus(): Promise<SystemHealthDTO> {
const [
databaseResult,
redisResult,
@@ -198,8 +198,8 @@ export class AdminPanelHealthService {
async getQueueMetrics(
queueName: MessageQueue,
timeRange: QueueMetricsTimeRange = QueueMetricsTimeRange.OneDay,
): Promise<QueueMetricsData> {
const redis = this.redisClient.getClient();
): Promise<QueueMetricsDataDTO> {
const redis = this.redisClient.getQueueClient();
const queue = new Queue(queueName, { connection: redis });
try {
@@ -325,7 +325,7 @@ export class AdminPanelHealthService {
timeRange: QueueMetricsTimeRange,
queueName: MessageQueue,
queueDetails: WorkerQueueHealth | null,
): QueueMetricsData {
): QueueMetricsDataDTO {
try {
return {
queueName,
@@ -0,0 +1,246 @@
import { Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { Queue } from 'bullmq';
import { type JobState as BullMQJobState } from 'bullmq/dist/esm/types';
import {
bullMQToJobStateEnum,
JobStateEnum,
jobStateEnumToBullMQ,
} from 'src/engine/core-modules/admin-panel/enums/job-state.enum';
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { QUEUE_RETENTION } from 'src/engine/core-modules/message-queue/constants/queue-retention.constants';
import { type MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { RedisClientService } from 'src/engine/core-modules/redis-client/redis-client.service';
type JobOperationResult = {
jobId: string;
success: boolean;
error?: string;
};
@Injectable()
export class AdminPanelQueueService {
constructor(private readonly redisClient: RedisClientService) {}
async getQueueJobs(
queueName: MessageQueue,
state: JobStateEnum,
limit = 50,
offset = 0,
) {
const redis = this.redisClient.getQueueClient();
const queue = new Queue(queueName, { connection: redis });
try {
const validLimit = Math.min(Math.max(1, limit), 200);
const validOffset = Math.max(0, offset);
const start = validOffset;
const end = validOffset + validLimit - 1;
// Convert GraphQL enum to BullMQ state
const bullMQState = jobStateEnumToBullMQ[state];
const jobs = await queue.getJobs([bullMQState], start, end, false);
const transformedJobs = await Promise.all(
jobs.map(async (job) => {
const jobBullMQState = (await job.getState()) as BullMQJobState;
return {
id: job.id!,
name: job.name,
data: job.data,
state: bullMQToJobStateEnum[jobBullMQState],
timestamp: job.timestamp,
failedReason: job.failedReason,
processedOn: job.processedOn,
finishedOn: job.finishedOn,
attemptsMade: job.attemptsMade,
returnValue: job.returnValue,
logs: undefined,
stackTrace: job.stackTrace,
};
}),
);
const hasMore = jobs.length === validLimit;
const jobCounts = await queue.getJobCounts(
'completed',
'failed',
'active',
'waiting',
'delayed',
'prioritized',
'waiting-children',
);
const totalCountForState = (() => {
switch (state) {
case JobStateEnum.COMPLETED:
return jobCounts.completed ?? 0;
case JobStateEnum.FAILED:
return jobCounts.failed ?? 0;
case JobStateEnum.ACTIVE:
return jobCounts.active ?? 0;
case JobStateEnum.WAITING:
return jobCounts.waiting ?? 0;
case JobStateEnum.DELAYED:
return jobCounts.delayed ?? 0;
case JobStateEnum.PRIORITIZED:
return jobCounts.prioritized ?? 0;
case JobStateEnum.WAITING_CHILDREN:
return jobCounts['waiting-children'] ?? 0;
default:
return jobs.length;
}
})();
return {
jobs: transformedJobs,
count: jobs.length,
totalCount: totalCountForState,
hasMore,
retentionConfig: { ...QUEUE_RETENTION },
};
} catch (error) {
throw new InternalServerError(
`Failed to fetch jobs from queue ${queueName}: ${error instanceof Error ? error.message : String(error)}`,
{
userFriendlyMessage: msg`Failed to load queue jobs. Please try again later.`,
},
);
} finally {
await queue.close();
}
}
async retryJobs(
queueName: MessageQueue,
jobIds: string[],
): Promise<{
retriedCount: number;
results: JobOperationResult[];
}> {
const redis = this.redisClient.getQueueClient();
const queue = new Queue(queueName, { connection: redis });
try {
if (jobIds.length === 0) {
await queue.retryJobs({ state: 'failed' });
return { retriedCount: -1, results: [] };
}
const results: JobOperationResult[] = [];
let retriedCount = 0;
for (const jobId of jobIds) {
const job = await queue.getJob(jobId);
if (!job) {
results.push({
jobId,
success: false,
error: 'Job not found',
});
continue;
}
const state = await job.getState();
if (state !== 'failed') {
results.push({
jobId,
success: false,
error: `Job is not in failed state (current state: ${state})`,
});
continue;
}
try {
await job.retry();
retriedCount++;
results.push({
jobId,
success: true,
});
} catch (error) {
results.push({
jobId,
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
}
return { retriedCount, results };
} catch (error) {
throw new InternalServerError(
`Failed to retry jobs in queue ${queueName}: ${error instanceof Error ? error.message : String(error)}`,
{
userFriendlyMessage: msg`Failed to retry jobs. Please try again later.`,
},
);
} finally {
await queue.close();
}
}
async deleteJobs(
queueName: MessageQueue,
jobIds: string[],
): Promise<{
deletedCount: number;
results: JobOperationResult[];
}> {
const redis = this.redisClient.getQueueClient();
const queue = new Queue(queueName, { connection: redis });
try {
const results: JobOperationResult[] = [];
let deletedCount = 0;
for (const jobId of jobIds) {
const job = await queue.getJob(jobId);
if (!job) {
results.push({
jobId,
success: false,
error: 'Job not found',
});
continue;
}
try {
await job.remove();
deletedCount++;
results.push({
jobId,
success: true,
});
} catch (error) {
results.push({
jobId,
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
}
return { deletedCount, results };
} catch (error) {
throw new InternalServerError(
`Failed to delete jobs in queue ${queueName}: ${error instanceof Error ? error.message : String(error)}`,
{
userFriendlyMessage: msg`Failed to delete jobs. Please try again later.`,
},
);
} finally {
await queue.close();
}
}
}
@@ -3,6 +3,7 @@ import { TerminusModule } from '@nestjs/terminus';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminPanelHealthService } from 'src/engine/core-modules/admin-panel/admin-panel-health.service';
import { AdminPanelQueueService } from 'src/engine/core-modules/admin-panel/admin-panel-queue.service';
import { AdminPanelResolver } from 'src/engine/core-modules/admin-panel/admin-panel.resolver';
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
@@ -32,7 +33,12 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
ImpersonationModule,
PermissionsModule,
],
providers: [AdminPanelResolver, AdminPanelService, AdminPanelHealthService],
providers: [
AdminPanelResolver,
AdminPanelService,
AdminPanelHealthService,
AdminPanelQueueService,
],
exports: [AdminPanelService],
})
export class AdminPanelModule {}
@@ -1,17 +1,22 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { AdminPanelHealthService } from 'src/engine/core-modules/admin-panel/admin-panel-health.service';
import { AdminPanelQueueService } from 'src/engine/core-modules/admin-panel/admin-panel-queue.service';
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
import { ConfigVariable } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
import { ConfigVariableDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
import { ConfigVariablesOutput } from 'src/engine/core-modules/admin-panel/dtos/config-variables.output';
import { SystemHealth } from 'src/engine/core-modules/admin-panel/dtos/system-health.dto';
import { DeleteJobsResponseDTO } from 'src/engine/core-modules/admin-panel/dtos/delete-jobs-response.dto';
import { QueueJobsResponseDTO } from 'src/engine/core-modules/admin-panel/dtos/queue-jobs-response.dto';
import { RetryJobsResponseDTO } from 'src/engine/core-modules/admin-panel/dtos/retry-jobs-response.dto';
import { SystemHealthDTO } from 'src/engine/core-modules/admin-panel/dtos/system-health.dto';
import { UpdateWorkspaceFeatureFlagInput } from 'src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input';
import { UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity';
import { UserLookupInput } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.input';
import { VersionInfo } from 'src/engine/core-modules/admin-panel/dtos/version-info.dto';
import { VersionInfoDTO } from 'src/engine/core-modules/admin-panel/dtos/version-info.dto';
import { JobStateEnum } from 'src/engine/core-modules/admin-panel/enums/job-state.enum';
import { QueueMetricsTimeRange } from 'src/engine/core-modules/admin-panel/enums/queue-metrics-time-range.enum';
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
import { FeatureFlagException } from 'src/engine/core-modules/feature-flag/feature-flag.exception';
@@ -29,8 +34,8 @@ import { ServerLevelImpersonateGuard } from 'src/engine/guards/server-level-impe
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { AdminPanelHealthServiceData } from './dtos/admin-panel-health-service-data.dto';
import { QueueMetricsData } from './dtos/queue-metrics-data.dto';
import { AdminPanelHealthServiceDataDTO } from './dtos/admin-panel-health-service-data.dto';
import { QueueMetricsDataDTO } from './dtos/queue-metrics-data.dto';
@UsePipes(ResolverValidationPipe)
@Resolver()
@@ -43,6 +48,7 @@ export class AdminPanelResolver {
constructor(
private adminService: AdminPanelService,
private adminPanelHealthService: AdminPanelHealthService,
private adminPanelQueueService: AdminPanelQueueService,
private featureFlagService: FeatureFlagService,
private readonly twentyConfigService: TwentyConfigService,
) {}
@@ -84,34 +90,34 @@ export class AdminPanelResolver {
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => SystemHealth)
async getSystemHealthStatus(): Promise<SystemHealth> {
@Query(() => SystemHealthDTO)
async getSystemHealthStatus(): Promise<SystemHealthDTO> {
return this.adminPanelHealthService.getSystemHealthStatus();
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => AdminPanelHealthServiceData)
@Query(() => AdminPanelHealthServiceDataDTO)
async getIndicatorHealthStatus(
@Args('indicatorId', {
type: () => HealthIndicatorId,
})
indicatorId: HealthIndicatorId,
): Promise<AdminPanelHealthServiceData> {
): Promise<AdminPanelHealthServiceDataDTO> {
return this.adminPanelHealthService.getIndicatorHealthStatus(indicatorId);
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => QueueMetricsData)
@Query(() => QueueMetricsDataDTO)
async getQueueMetrics(
@Args('queueName', { type: () => String })
queueName: string,
@Args('timeRange', {
nullable: true,
defaultValue: QueueMetricsTimeRange.OneDay,
defaultValue: QueueMetricsTimeRange.OneHour,
type: () => QueueMetricsTimeRange,
})
timeRange: QueueMetricsTimeRange = QueueMetricsTimeRange.OneHour,
): Promise<QueueMetricsData> {
): Promise<QueueMetricsDataDTO> {
return await this.adminPanelHealthService.getQueueMetrics(
queueName as MessageQueue,
timeRange,
@@ -119,16 +125,16 @@ export class AdminPanelResolver {
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => VersionInfo)
async versionInfo(): Promise<VersionInfo> {
@Query(() => VersionInfoDTO)
async versionInfo(): Promise<VersionInfoDTO> {
return this.adminService.getVersionInfo();
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => ConfigVariable)
@Query(() => ConfigVariableDTO)
async getDatabaseConfigVariable(
@Args('key', { type: () => String }) key: keyof ConfigVariables,
): Promise<ConfigVariable> {
): Promise<ConfigVariableDTO> {
this.twentyConfigService.validateConfigVariableExists(key as string);
return this.adminService.getConfigVariable(key);
@@ -167,4 +173,52 @@ export class AdminPanelResolver {
return true;
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => QueueJobsResponseDTO)
async getQueueJobs(
@Args('queueName', { type: () => String })
queueName: string,
@Args('state', { type: () => JobStateEnum })
state: JobStateEnum,
@Args('limit', { type: () => Int, nullable: true, defaultValue: 50 })
limit?: number,
@Args('offset', { type: () => Int, nullable: true, defaultValue: 0 })
offset?: number,
): Promise<QueueJobsResponseDTO> {
return await this.adminPanelQueueService.getQueueJobs(
queueName as MessageQueue,
state,
limit,
offset,
);
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Mutation(() => RetryJobsResponseDTO)
async retryJobs(
@Args('queueName', { type: () => String })
queueName: string,
@Args('jobIds', { type: () => [String] })
jobIds: string[],
): Promise<RetryJobsResponseDTO> {
return await this.adminPanelQueueService.retryJobs(
queueName as MessageQueue,
jobIds,
);
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Mutation(() => DeleteJobsResponseDTO)
async deleteJobs(
@Args('queueName', { type: () => String })
queueName: string,
@Args('jobIds', { type: () => [String] })
jobIds: string[],
): Promise<DeleteJobsResponseDTO> {
return await this.adminPanelQueueService.deleteJobs(
queueName as MessageQueue,
jobIds,
);
}
}
@@ -7,11 +7,11 @@ import semver from 'semver';
import { Repository } from 'typeorm';
import * as z from 'zod';
import { type ConfigVariable } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
import { type ConfigVariablesGroupData } from 'src/engine/core-modules/admin-panel/dtos/config-variables-group.dto';
import { type ConfigVariableDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
import { type ConfigVariablesGroupDataDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variables-group.dto';
import { type ConfigVariablesOutput } from 'src/engine/core-modules/admin-panel/dtos/config-variables.output';
import { type UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity';
import { type VersionInfo } from 'src/engine/core-modules/admin-panel/dtos/version-info.dto';
import { type VersionInfoDTO } from 'src/engine/core-modules/admin-panel/dtos/version-info.dto';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import {
AuthException,
@@ -114,14 +114,14 @@ export class AdminPanelService {
getConfigVariablesGrouped(): ConfigVariablesOutput {
const rawEnvVars = this.twentyConfigService.getAll();
const groupedData = new Map<ConfigVariablesGroup, ConfigVariable[]>();
const groupedData = new Map<ConfigVariablesGroup, ConfigVariableDTO[]>();
for (const [varName, { value, metadata, source }] of Object.entries(
rawEnvVars,
)) {
const { group, description } = metadata;
const envVar: ConfigVariable = {
const envVar: ConfigVariableDTO = {
name: varName,
description,
value: value ?? null,
@@ -139,7 +139,9 @@ export class AdminPanelService {
groupedData.get(group)?.push(envVar);
}
const groups: ConfigVariablesGroupData[] = Array.from(groupedData.entries())
const groups: ConfigVariablesGroupDataDTO[] = Array.from(
groupedData.entries(),
)
.sort((a, b) => {
const positionA = CONFIG_VARIABLES_GROUP_METADATA[a[0]].position;
const positionB = CONFIG_VARIABLES_GROUP_METADATA[b[0]].position;
@@ -156,7 +158,7 @@ export class AdminPanelService {
return { groups };
}
getConfigVariable(key: string): ConfigVariable {
getConfigVariable(key: string): ConfigVariableDTO {
const variableWithMetadata =
this.twentyConfigService.getVariableWithMetadata(
key as keyof ConfigVariables,
@@ -180,7 +182,7 @@ export class AdminPanelService {
};
}
async getVersionInfo(): Promise<VersionInfo> {
async getVersionInfo(): Promise<VersionInfoDTO> {
const currentVersion = this.twentyConfigService.get('APP_VERSION');
try {
@@ -1,11 +1,11 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { AdminPanelWorkerQueueHealth } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-worker-queue-health.dto';
import { AdminPanelWorkerQueueHealthDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-worker-queue-health.dto';
import { AdminPanelHealthServiceStatus } from 'src/engine/core-modules/admin-panel/enums/admin-panel-health-service-status.enum';
import { HealthIndicatorId } from 'src/engine/core-modules/health/enums/health-indicator-id.enum';
@ObjectType()
export class AdminPanelHealthServiceData {
@ObjectType('AdminPanelHealthServiceData')
export class AdminPanelHealthServiceDataDTO {
@Field(() => HealthIndicatorId)
id: HealthIndicatorId;
@@ -23,6 +23,6 @@ export class AdminPanelHealthServiceData {
@Field(() => String, { nullable: true })
details?: string;
@Field(() => [AdminPanelWorkerQueueHealth], { nullable: true })
queues?: AdminPanelWorkerQueueHealth[];
@Field(() => [AdminPanelWorkerQueueHealthDTO], { nullable: true })
queues?: AdminPanelWorkerQueueHealthDTO[];
}
@@ -2,8 +2,8 @@ import { Field, ObjectType } from '@nestjs/graphql';
import { AdminPanelHealthServiceStatus } from 'src/engine/core-modules/admin-panel/enums/admin-panel-health-service-status.enum';
@ObjectType()
export class AdminPanelWorkerQueueHealth {
@ObjectType('AdminPanelWorkerQueueHealth')
export class AdminPanelWorkerQueueHealthDTO {
@Field(() => String)
id: string;
@@ -15,8 +15,8 @@ registerEnumType(ConfigVariableType, {
name: 'ConfigVariableType',
});
@ObjectType()
export class ConfigVariable {
@ObjectType('ConfigVariable')
export class ConfigVariableDTO {
@Field()
name: string;
@@ -1,16 +1,16 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { ConfigVariable } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
import { ConfigVariableDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
registerEnumType(ConfigVariablesGroup, {
name: 'ConfigVariablesGroup',
});
@ObjectType()
export class ConfigVariablesGroupData {
@Field(() => [ConfigVariable])
variables: ConfigVariable[];
@ObjectType('ConfigVariablesGroupData')
export class ConfigVariablesGroupDataDTO {
@Field(() => [ConfigVariableDTO])
variables: ConfigVariableDTO[];
@Field(() => ConfigVariablesGroup)
name: ConfigVariablesGroup;
@@ -1,9 +1,9 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { ConfigVariablesGroupData } from 'src/engine/core-modules/admin-panel/dtos/config-variables-group.dto';
import { ConfigVariablesGroupDataDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variables-group.dto';
@ObjectType()
@ObjectType('ConfigVariablesOutput')
export class ConfigVariablesOutput {
@Field(() => [ConfigVariablesGroupData])
groups: ConfigVariablesGroupData[];
@Field(() => [ConfigVariablesGroupDataDTO])
groups: ConfigVariablesGroupDataDTO[];
}
@@ -0,0 +1,12 @@
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { JobOperationResultDTO } from 'src/engine/core-modules/admin-panel/dtos/job-operation-result.dto';
@ObjectType('DeleteJobsResponse')
export class DeleteJobsResponseDTO {
@Field(() => Int)
deletedCount: number;
@Field(() => [JobOperationResultDTO])
results: JobOperationResultDTO[];
}
@@ -1,13 +1,13 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { AuthToken } from 'src/engine/core-modules/auth/dto/token.entity';
import { WorkspaceUrlsAndId } from 'src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto';
import { WorkspaceUrlsAndIdDTO } from 'src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto';
@ObjectType()
@ObjectType('ImpersonateOutput')
export class ImpersonateOutput {
@Field(() => AuthToken)
loginToken: AuthToken;
@Field(() => WorkspaceUrlsAndId)
workspace: WorkspaceUrlsAndId;
@Field(() => WorkspaceUrlsAndIdDTO)
workspace: WorkspaceUrlsAndIdDTO;
}
@@ -0,0 +1,13 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType('JobOperationResult')
export class JobOperationResultDTO {
@Field(() => String)
jobId: string;
@Field(() => Boolean)
success: boolean;
@Field(() => String, { nullable: true })
error?: string;
}
@@ -0,0 +1,44 @@
import { Field, ObjectType } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { JobStateEnum } from 'src/engine/core-modules/admin-panel/enums/job-state.enum';
@ObjectType('QueueJob')
export class QueueJobDTO {
@Field(() => String)
id: string;
@Field(() => String)
name: string;
@Field(() => GraphQLJSON, { nullable: true })
data?: object;
@Field(() => JobStateEnum)
state: JobStateEnum;
@Field(() => Number, { nullable: true })
timestamp?: number;
@Field(() => String, { nullable: true })
failedReason?: string;
@Field(() => Number, { nullable: true })
processedOn?: number;
@Field(() => Number, { nullable: true })
finishedOn?: number;
@Field(() => Number)
attemptsMade: number;
@Field(() => GraphQLJSON, { nullable: true })
returnValue?: object;
@Field(() => [String], { nullable: true })
logs?: string[];
@Field(() => [String], { nullable: true })
stackTrace?: string[];
}
@@ -0,0 +1,22 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { QueueJobDTO } from 'src/engine/core-modules/admin-panel/dtos/queue-job.dto';
import { QueueRetentionConfigDTO } from 'src/engine/core-modules/admin-panel/dtos/queue-retention-config.dto';
@ObjectType('QueueJobsResponse')
export class QueueJobsResponseDTO {
@Field(() => [QueueJobDTO])
jobs: QueueJobDTO[];
@Field(() => Number)
count: number;
@Field(() => Number)
totalCount: number;
@Field(() => Boolean)
hasMore: boolean;
@Field(() => QueueRetentionConfigDTO)
retentionConfig: QueueRetentionConfigDTO;
}
@@ -1,7 +1,7 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class QueueMetricsDataPoint {
@ObjectType('QueueMetricsDataPoint')
export class QueueMetricsDataPointDTO {
@Field(() => Number)
x: number;
@@ -1,11 +1,11 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { QueueMetricsSeries } from 'src/engine/core-modules/admin-panel/dtos/queue-metrics-series.dto';
import { QueueMetricsSeriesDTO } from 'src/engine/core-modules/admin-panel/dtos/queue-metrics-series.dto';
import { QueueMetricsTimeRange } from 'src/engine/core-modules/admin-panel/enums/queue-metrics-time-range.enum';
import { WorkerQueueMetrics } from 'src/engine/core-modules/health/types/worker-queue-metrics.type';
@ObjectType()
export class QueueMetricsData {
@ObjectType('QueueMetricsData')
export class QueueMetricsDataDTO {
@Field(() => String)
queueName: string;
@@ -18,6 +18,6 @@ export class QueueMetricsData {
@Field(() => WorkerQueueMetrics, { nullable: true })
details: WorkerQueueMetrics | null;
@Field(() => [QueueMetricsSeries])
data: QueueMetricsSeries[];
@Field(() => [QueueMetricsSeriesDTO])
data: QueueMetricsSeriesDTO[];
}
@@ -1,12 +1,12 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { QueueMetricsDataPoint } from 'src/engine/core-modules/admin-panel/dtos/queue-metrics-data-point.dto';
import { QueueMetricsDataPointDTO } from 'src/engine/core-modules/admin-panel/dtos/queue-metrics-data-point.dto';
@ObjectType()
export class QueueMetricsSeries {
@ObjectType('QueueMetricsSeries')
export class QueueMetricsSeriesDTO {
@Field(() => String)
id: string;
@Field(() => [QueueMetricsDataPoint])
data: QueueMetricsDataPoint[];
@Field(() => [QueueMetricsDataPointDTO])
data: QueueMetricsDataPointDTO[];
}
@@ -0,0 +1,16 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType('QueueRetentionConfig')
export class QueueRetentionConfigDTO {
@Field(() => Number)
completedMaxAge: number;
@Field(() => Number)
completedMaxCount: number;
@Field(() => Number)
failedMaxAge: number;
@Field(() => Number)
failedMaxCount: number;
}
@@ -0,0 +1,12 @@
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { JobOperationResultDTO } from 'src/engine/core-modules/admin-panel/dtos/job-operation-result.dto';
@ObjectType('RetryJobsResponse')
export class RetryJobsResponseDTO {
@Field(() => Int)
retriedCount: number;
@Field(() => [JobOperationResultDTO])
results: JobOperationResultDTO[];
}
@@ -3,8 +3,8 @@ import { Field, ObjectType } from '@nestjs/graphql';
import { AdminPanelHealthServiceStatus } from 'src/engine/core-modules/admin-panel/enums/admin-panel-health-service-status.enum';
import { HealthIndicatorId } from 'src/engine/core-modules/health/enums/health-indicator-id.enum';
@ObjectType()
export class SystemHealthService {
@ObjectType('SystemHealthService')
export class SystemHealthServiceDTO {
@Field(() => HealthIndicatorId)
id: HealthIndicatorId;
@@ -15,8 +15,8 @@ export class SystemHealthService {
status: AdminPanelHealthServiceStatus;
}
@ObjectType()
export class SystemHealth {
@Field(() => [SystemHealthService])
services: SystemHealthService[];
@ObjectType('SystemHealth')
export class SystemHealthDTO {
@Field(() => [SystemHealthServiceDTO])
services: SystemHealthServiceDTO[];
}
@@ -2,10 +2,10 @@ import { Field, ObjectType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { WorkspaceUrls } from 'src/engine/core-modules/workspace/dtos/workspace-urls.dto';
import { WorkspaceUrlsDTO } from 'src/engine/core-modules/workspace/dtos/workspace-urls.dto';
@ObjectType()
class UserInfo {
@ObjectType('UserInfo')
class UserInfoDTO {
@Field(() => UUIDScalarType)
id: string;
@@ -19,8 +19,8 @@ class UserInfo {
lastName?: string;
}
@ObjectType()
class WorkspaceInfo {
@ObjectType('WorkspaceInfo')
class WorkspaceInfoDTO {
@Field(() => UUIDScalarType)
id: string;
@@ -36,21 +36,21 @@ class WorkspaceInfo {
@Field(() => Number)
totalUsers: number;
@Field(() => WorkspaceUrls)
workspaceUrls: WorkspaceUrls;
@Field(() => WorkspaceUrlsDTO)
workspaceUrls: WorkspaceUrlsDTO;
@Field(() => [UserInfo])
users: UserInfo[];
@Field(() => [UserInfoDTO])
users: UserInfoDTO[];
@Field(() => [FeatureFlag])
featureFlags: FeatureFlag[];
}
@ObjectType()
@ObjectType('UserLookup')
export class UserLookup {
@Field(() => UserInfo)
user: UserInfo;
@Field(() => UserInfoDTO)
user: UserInfoDTO;
@Field(() => [WorkspaceInfo])
workspaces: WorkspaceInfo[];
@Field(() => [WorkspaceInfoDTO])
workspaces: WorkspaceInfoDTO[];
}
@@ -1,7 +1,7 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class VersionInfo {
@ObjectType('VersionInfo')
export class VersionInfoDTO {
@Field(() => String, { nullable: true })
currentVersion?: string;
@@ -0,0 +1,40 @@
import { registerEnumType } from '@nestjs/graphql';
import { type JobState as BullMQJobState } from 'bullmq/dist/esm/types';
export enum JobStateEnum {
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
ACTIVE = 'ACTIVE',
WAITING = 'WAITING',
DELAYED = 'DELAYED',
PRIORITIZED = 'PRIORITIZED',
WAITING_CHILDREN = 'WAITING_CHILDREN',
}
registerEnumType(JobStateEnum, {
name: 'JobState',
description: 'Job state in the queue',
});
// Mapping from GraphQL enum to BullMQ state values
export const jobStateEnumToBullMQ: Record<JobStateEnum, BullMQJobState> = {
[JobStateEnum.COMPLETED]: 'completed',
[JobStateEnum.FAILED]: 'failed',
[JobStateEnum.ACTIVE]: 'active',
[JobStateEnum.WAITING]: 'waiting',
[JobStateEnum.DELAYED]: 'delayed',
[JobStateEnum.PRIORITIZED]: 'prioritized',
[JobStateEnum.WAITING_CHILDREN]: 'waiting-children',
};
// Mapping from BullMQ state values to GraphQL enum
export const bullMQToJobStateEnum: Record<BullMQJobState, JobStateEnum> = {
completed: JobStateEnum.COMPLETED,
failed: JobStateEnum.FAILED,
active: JobStateEnum.ACTIVE,
waiting: JobStateEnum.WAITING,
delayed: JobStateEnum.DELAYED,
prioritized: JobStateEnum.PRIORITIZED,
'waiting-children': JobStateEnum.WAITING_CHILDREN,
};
@@ -8,10 +8,10 @@ import {
IdentityProviderType,
SSOIdentityProviderStatus,
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { WorkspaceUrls } from 'src/engine/core-modules/workspace/dtos/workspace-urls.dto';
import { WorkspaceUrlsDTO } from 'src/engine/core-modules/workspace/dtos/workspace-urls.dto';
@ObjectType()
class SSOConnection {
@ObjectType('SSOConnection')
class SSOConnectionDTO {
@Field(() => IdentityProviderType)
type: SSOConfiguration['type'];
@@ -28,7 +28,7 @@ class SSOConnection {
status: SSOConfiguration['status'];
}
@ObjectType()
@ObjectType('AvailableWorkspace')
export class AvailableWorkspace {
@Field(() => UUIDScalarType)
id: string;
@@ -45,17 +45,17 @@ export class AvailableWorkspace {
@Field(() => String, { nullable: true })
inviteHash?: string;
@Field(() => WorkspaceUrls)
workspaceUrls: WorkspaceUrls;
@Field(() => WorkspaceUrlsDTO)
workspaceUrls: WorkspaceUrlsDTO;
@Field(() => String, { nullable: true })
logo?: string;
@Field(() => [SSOConnection])
sso: SSOConnection[];
@Field(() => [SSOConnectionDTO])
sso: SSOConnectionDTO[];
}
@ObjectType()
@ObjectType('AvailableWorkspaces')
export class AvailableWorkspaces {
@Field(() => [AvailableWorkspace])
availableWorkspacesForSignIn: Array<AvailableWorkspace>;
@@ -1,14 +1,14 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { WorkspaceUrls } from 'src/engine/core-modules/workspace/dtos/workspace-urls.dto';
import { WorkspaceUrlsDTO } from 'src/engine/core-modules/workspace/dtos/workspace-urls.dto';
import { AuthToken } from './token.entity';
@ObjectType()
@ObjectType('GetLoginTokenFromEmailVerificationTokenOutput')
export class GetLoginTokenFromEmailVerificationTokenOutput {
@Field(() => AuthToken)
loginToken: AuthToken;
@Field(() => WorkspaceUrls)
workspaceUrls: WorkspaceUrls;
@Field(() => WorkspaceUrlsDTO)
workspaceUrls: WorkspaceUrlsDTO;
}
@@ -1,14 +1,14 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { WorkspaceUrlsAndId } from 'src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto';
import { WorkspaceUrlsAndIdDTO } from 'src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto';
import { AuthToken } from './token.entity';
@ObjectType()
@ObjectType('SignUpOutput')
export class SignUpOutput {
@Field(() => AuthToken)
loginToken: AuthToken;
@Field(() => WorkspaceUrlsAndId)
workspace: WorkspaceUrlsAndId;
@Field(() => WorkspaceUrlsAndIdDTO)
workspace: WorkspaceUrlsAndIdDTO;
}
@@ -4,11 +4,11 @@ import { isDefined } from 'twenty-shared/utils';
import type Stripe from 'stripe';
import { type BillingSubscriptionSchedulePhase } from 'src/engine/core-modules/billing/dtos/billing-subscription-schedule-phase.dto';
import { type BillingSubscriptionSchedulePhaseDTO } from 'src/engine/core-modules/billing/dtos/billing-subscription-schedule-phase.dto';
export function transformStripeSubscriptionScheduleEventToDatabaseSubscriptionPhase(
schedule: Stripe.SubscriptionSchedule,
): Array<BillingSubscriptionSchedulePhase> {
): Array<BillingSubscriptionSchedulePhaseDTO> {
return schedule.phases.slice(-2).map((phase) => ({
start_date: phase.start_date,
end_date: phase.end_date,
@@ -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;
@@ -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[];
@@ -2,7 +2,7 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
@ObjectType('BillingPriceTier')
export class BillingPriceTierDTO {
@Field(() => Number, { nullable: true })
upTo: number | null;
@@ -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>;
}
@@ -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)
@@ -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;
@@ -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(
@@ -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;
@@ -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',
@@ -9,7 +9,7 @@ import {
import { BillingTrialPeriodDTO } from 'src/engine/core-modules/billing/dtos/billing-trial-period.dto';
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
import { AuthProvidersDTO } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
registerEnumType(FeatureFlagKey, {
name: 'FeatureFlagKey',
@@ -123,8 +123,8 @@ export class ClientConfig {
@Field(() => String, { nullable: true })
appVersion?: string;
@Field(() => AuthProviders, { nullable: false })
authProviders: AuthProviders;
@Field(() => AuthProvidersDTO, { nullable: false })
authProviders: AuthProvidersDTO;
@Field(() => Billing, { nullable: false })
billing: Billing;
@@ -24,7 +24,7 @@ export class EmailDriverFactory extends DriverFactoryBase<EmailDriverInterface>
if (driver === EmailDriver.SMTP) {
const emailConfigHash = this.getConfigGroupHash(
ConfigVariablesGroup.EmailSettings,
ConfigVariablesGroup.EMAIL_SETTINGS,
);
return `smtp|${emailConfigHash}`;
@@ -19,7 +19,7 @@ import {
import { type AwsSesClientProvider } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/providers/aws-ses-client.provider';
import { type AwsSesHandleErrorService } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-handle-error.service';
import { EmailingDomainStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain';
import { type VerificationRecord } from 'src/engine/core-modules/emailing-domain/dtos/verification-record.dto';
import { type VerificationRecordDTO } from 'src/engine/core-modules/emailing-domain/dtos/verification-record.dto';
export class AwsSesDriver implements EmailingDomainDriverInterface {
private readonly logger = new Logger(AwsSesDriver.name);
@@ -127,7 +127,7 @@ export class AwsSesDriver implements EmailingDomainDriverInterface {
tenantName: string,
): Promise<{
isVerified: boolean;
verificationRecords: VerificationRecord[];
verificationRecords: VerificationRecordDTO[];
}> {
const sesClient = this.awsSesClientProvider.getSESClient();
@@ -161,7 +161,7 @@ export class AwsSesDriver implements EmailingDomainDriverInterface {
tenantName: string,
): Promise<{
isVerified: boolean;
verificationRecords: VerificationRecord[];
verificationRecords: VerificationRecordDTO[];
}> {
const sesClient = this.awsSesClientProvider.getSESClient();
@@ -227,7 +227,7 @@ export class AwsSesDriver implements EmailingDomainDriverInterface {
private buildVerificationRecords(
domain: string,
dkimTokens: string[],
): VerificationRecord[] {
): VerificationRecordDTO[] {
return dkimTokens.map((token) => ({
type: 'CNAME' as const,
key: `${token}._domainkey.${domain}`,
@@ -26,7 +26,7 @@ export class EmailingDomainDriverFactory extends DriverFactoryBase<EmailingDomai
if (driver === EmailingDomainDriver.AWS_SES) {
const awsConfigHash = this.getConfigGroupHash(
ConfigVariablesGroup.AwsSesSettings,
ConfigVariablesGroup.AWS_SES_SETTINGS,
);
return `aws-ses|${awsConfigHash}`;
@@ -7,7 +7,7 @@ import {
EmailingDomainDriver,
EmailingDomainStatus,
} from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain';
import { VerificationRecord } from 'src/engine/core-modules/emailing-domain/dtos/verification-record.dto';
import { VerificationRecordDTO } from 'src/engine/core-modules/emailing-domain/dtos/verification-record.dto';
registerEnumType(EmailingDomainDriver, {
name: 'EmailingDomainDriver',
@@ -37,8 +37,8 @@ export class EmailingDomainDto {
@Field(() => EmailingDomainStatus)
status: EmailingDomainStatus;
@Field(() => [VerificationRecord], { nullable: true })
verificationRecords: VerificationRecord[] | null;
@Field(() => [VerificationRecordDTO], { nullable: true })
verificationRecords: VerificationRecordDTO[] | null;
@Field(() => Date, { nullable: true })
verifiedAt: Date | null;
@@ -1,7 +1,7 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class VerificationRecord {
@ObjectType('VerificationRecord')
export class VerificationRecordDTO {
@Field(() => String)
type: 'TXT' | 'CNAME' | 'MX';
@@ -29,7 +29,7 @@ export class FileStorageDriverFactory extends DriverFactoryBase<StorageDriver> {
if (storageType === StorageDriverType.S_3) {
const storageConfigHash = this.getConfigGroupHash(
ConfigVariablesGroup.StorageConfig,
ConfigVariablesGroup.STORAGE_CONFIG,
);
return `s3|${storageConfigHash}`;
@@ -1,6 +1,6 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
@ObjectType('SignedFile')
export class SignedFileDTO {
@Field(() => String)
path: string;

Some files were not shown because too many files have changed in this diff Show More