Add queue management dashboard (#15202)
Adds a comprehensive queue management interface to the admin panel for viewing and managing background jobs. **Features:** - Queue detail pages showing paginated job lists (50 per page) - Filter jobs by state: completed, failed, active, waiting, delayed, paused - Checkbox selection with bulk actions (delete jobs, retry failed jobs) - Per-job dropdown menu for individual retry/delete - Expandable rows showing error messages, stack traces, and job data - Relative timestamps with hover tooltips - Display attempt counts on failed jobs - Dynamic retention policy info from backend **Changes:** - Backend: New AdminPanelQueueService with GraphQL endpoints for job listing, retry, and delete - Frontend: Queue detail page with QueueJobsTable component - Updated retention policy: completed jobs kept 4 hours, failed jobs kept 7 days (max 1000 each) - Added JobState enum for type safety <img width="634" height="696" alt="Screenshot_2025-10-20_at_11 45 25" src="https://github.com/user-attachments/assets/c67bcd27-26cf-47f5-9575-3cd5684d006b" /> <img width="484" height="680" alt="Screenshot_2025-10-20_at_11 45 14" src="https://github.com/user-attachments/assets/68725cc6-b3ec-4098-99ca-f9a717d6f8f1" /> <img width="490" height="643" alt="Screenshot_2025-10-20_at_11 45 05" src="https://github.com/user-attachments/assets/b68a5809-33ff-4452-b48b-741aff7f1dd6" /> <img width="685" height="662" alt="Screenshot 2025-10-20 at 13 15 01" src="https://github.com/user-attachments/assets/eeb5207b-de5c-4b18-bdde-392892053dab" />
This commit is contained in:
@@ -115,8 +115,8 @@ export type AgentChatThread = {
|
||||
updatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export type AgentHandoffDto = {
|
||||
__typename?: 'AgentHandoffDTO';
|
||||
export type AgentHandoff = {
|
||||
__typename?: 'AgentHandoff';
|
||||
description?: Maybe<Scalars['String']>;
|
||||
id: Scalars['UUID'];
|
||||
toAgent: Agent;
|
||||
@@ -261,8 +261,8 @@ export type AuthorizeApp = {
|
||||
redirectUrl: Scalars['String'];
|
||||
};
|
||||
|
||||
export type AutocompleteResultDto = {
|
||||
__typename?: 'AutocompleteResultDto';
|
||||
export type AutocompleteResult = {
|
||||
__typename?: 'AutocompleteResult';
|
||||
placeId: Scalars['String'];
|
||||
text: Scalars['String'];
|
||||
};
|
||||
@@ -331,7 +331,7 @@ export type Billing = {
|
||||
__typename?: 'Billing';
|
||||
billingUrl?: Maybe<Scalars['String']>;
|
||||
isBillingEnabled: Scalars['Boolean'];
|
||||
trialPeriods: Array<BillingTrialPeriodDto>;
|
||||
trialPeriods: Array<BillingTrialPeriod>;
|
||||
};
|
||||
|
||||
export type BillingEndTrialPeriodOutput = {
|
||||
@@ -348,7 +348,7 @@ export type BillingLicensedProduct = BillingProductDto & {
|
||||
images?: Maybe<Array<Scalars['String']>>;
|
||||
metadata: BillingProductMetadata;
|
||||
name: Scalars['String'];
|
||||
prices?: Maybe<Array<BillingPriceLicensedDto>>;
|
||||
prices?: Maybe<Array<BillingPriceLicensed>>;
|
||||
};
|
||||
|
||||
export type BillingMeteredProduct = BillingProductDto & {
|
||||
@@ -357,7 +357,7 @@ export type BillingMeteredProduct = BillingProductDto & {
|
||||
images?: Maybe<Array<Scalars['String']>>;
|
||||
metadata: BillingProductMetadata;
|
||||
name: Scalars['String'];
|
||||
prices?: Maybe<Array<BillingPriceMeteredDto>>;
|
||||
prices?: Maybe<Array<BillingPriceMetered>>;
|
||||
};
|
||||
|
||||
export type BillingMeteredProductUsageOutput = {
|
||||
@@ -383,24 +383,24 @@ export type BillingPlanOutput = {
|
||||
planKey: BillingPlanKey;
|
||||
};
|
||||
|
||||
export type BillingPriceLicensedDto = {
|
||||
__typename?: 'BillingPriceLicensedDTO';
|
||||
export type BillingPriceLicensed = {
|
||||
__typename?: 'BillingPriceLicensed';
|
||||
priceUsageType: BillingUsageType;
|
||||
recurringInterval: SubscriptionInterval;
|
||||
stripePriceId: Scalars['String'];
|
||||
unitAmount: Scalars['Float'];
|
||||
};
|
||||
|
||||
export type BillingPriceMeteredDto = {
|
||||
__typename?: 'BillingPriceMeteredDTO';
|
||||
export type BillingPriceMetered = {
|
||||
__typename?: 'BillingPriceMetered';
|
||||
priceUsageType: BillingUsageType;
|
||||
recurringInterval: SubscriptionInterval;
|
||||
stripePriceId: Scalars['String'];
|
||||
tiers: Array<BillingPriceTierDto>;
|
||||
tiers: Array<BillingPriceTier>;
|
||||
};
|
||||
|
||||
export type BillingPriceTierDto = {
|
||||
__typename?: 'BillingPriceTierDTO';
|
||||
export type BillingPriceTier = {
|
||||
__typename?: 'BillingPriceTier';
|
||||
flatAmount?: Maybe<Scalars['Float']>;
|
||||
unitAmount?: Maybe<Scalars['Float']>;
|
||||
upTo?: Maybe<Scalars['Float']>;
|
||||
@@ -472,8 +472,8 @@ export type BillingSubscriptionSchedulePhaseItem = {
|
||||
quantity?: Maybe<Scalars['Float']>;
|
||||
};
|
||||
|
||||
export type BillingTrialPeriodDto = {
|
||||
__typename?: 'BillingTrialPeriodDTO';
|
||||
export type BillingTrialPeriod = {
|
||||
__typename?: 'BillingTrialPeriod';
|
||||
duration: Scalars['Float'];
|
||||
isCreditCardRequired: Scalars['Boolean'];
|
||||
};
|
||||
@@ -568,27 +568,27 @@ export enum ConfigVariableType {
|
||||
}
|
||||
|
||||
export enum ConfigVariablesGroup {
|
||||
AnalyticsConfig = 'AnalyticsConfig',
|
||||
AwsSesSettings = 'AwsSesSettings',
|
||||
BillingConfig = 'BillingConfig',
|
||||
CaptchaConfig = 'CaptchaConfig',
|
||||
CloudflareConfig = 'CloudflareConfig',
|
||||
EmailSettings = 'EmailSettings',
|
||||
ExceptionHandler = 'ExceptionHandler',
|
||||
GoogleAuth = 'GoogleAuth',
|
||||
ANALYTICS_CONFIG = 'ANALYTICS_CONFIG',
|
||||
AWS_SES_SETTINGS = 'AWS_SES_SETTINGS',
|
||||
BILLING_CONFIG = 'BILLING_CONFIG',
|
||||
CAPTCHA_CONFIG = 'CAPTCHA_CONFIG',
|
||||
CLOUDFLARE_CONFIG = 'CLOUDFLARE_CONFIG',
|
||||
EMAIL_SETTINGS = 'EMAIL_SETTINGS',
|
||||
EXCEPTION_HANDLER = 'EXCEPTION_HANDLER',
|
||||
GOOGLE_AUTH = 'GOOGLE_AUTH',
|
||||
LLM = 'LLM',
|
||||
Logging = 'Logging',
|
||||
Metering = 'Metering',
|
||||
MicrosoftAuth = 'MicrosoftAuth',
|
||||
Other = 'Other',
|
||||
RateLimiting = 'RateLimiting',
|
||||
LOGGING = 'LOGGING',
|
||||
METERING = 'METERING',
|
||||
MICROSOFT_AUTH = 'MICROSOFT_AUTH',
|
||||
OTHER = 'OTHER',
|
||||
RATE_LIMITING = 'RATE_LIMITING',
|
||||
SERVERLESS_CONFIG = 'SERVERLESS_CONFIG',
|
||||
SERVER_CONFIG = 'SERVER_CONFIG',
|
||||
SSL = 'SSL',
|
||||
ServerConfig = 'ServerConfig',
|
||||
ServerlessConfig = 'ServerlessConfig',
|
||||
StorageConfig = 'StorageConfig',
|
||||
SupportChatConfig = 'SupportChatConfig',
|
||||
TokensDuration = 'TokensDuration',
|
||||
TwoFactorAuthentication = 'TwoFactorAuthentication'
|
||||
STORAGE_CONFIG = 'STORAGE_CONFIG',
|
||||
SUPPORT_CHAT_CONFIG = 'SUPPORT_CHAT_CONFIG',
|
||||
TOKENS_DURATION = 'TOKENS_DURATION',
|
||||
TWO_FACTOR_AUTHENTICATION = 'TWO_FACTOR_AUTHENTICATION'
|
||||
}
|
||||
|
||||
export type ConfigVariablesGroupData = {
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
-1
@@ -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
-1
@@ -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
|
||||
|
||||
+6
-5
@@ -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
-13
@@ -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
|
||||
+38
@@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -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}
|
||||
/>
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ const HealthStatusIcons: { [k in HealthIndicatorId]: IconComponent } = {
|
||||
[HealthIndicatorId.app]: IconAppWindow,
|
||||
};
|
||||
|
||||
export const SettingsHealthStatusListCard = ({
|
||||
export const SettingsAdminHealthStatusListCard = ({
|
||||
services,
|
||||
loading,
|
||||
}: {
|
||||
+6
-6
@@ -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;
|
||||
|
||||
+140
@@ -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>
|
||||
);
|
||||
};
|
||||
+52
@@ -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>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -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();
|
||||
|
||||
+69
@@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+412
@@ -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>
|
||||
);
|
||||
};
|
||||
+38
@@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
+6
-3
@@ -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}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
+7
-5
@@ -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={[
|
||||
{
|
||||
+4
-4
@@ -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) => (
|
||||
+88
@@ -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>
|
||||
);
|
||||
};
|
||||
-68
@@ -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>
|
||||
);
|
||||
};
|
||||
+14
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
+14
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
+41
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
+83
@@ -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,
|
||||
};
|
||||
};
|
||||
+87
@@ -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,
|
||||
};
|
||||
};
|
||||
+17
-2
@@ -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}
|
||||
|
||||
+5
-1
@@ -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}
|
||||
|
||||
+9
-6
@@ -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>
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+1
-1
@@ -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
-8
@@ -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;
|
||||
|
||||
+6
-6
@@ -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({
|
||||
|
||||
+11
-10
@@ -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(
|
||||
|
||||
+4
-16
@@ -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,
|
||||
|
||||
+4
-2
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
+8
-8
@@ -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,
|
||||
|
||||
+246
@@ -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 {}
|
||||
|
||||
+71
-17
@@ -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 {
|
||||
|
||||
+5
-5
@@ -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
-2
@@ -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;
|
||||
|
||||
|
||||
+2
-2
@@ -15,8 +15,8 @@ registerEnumType(ConfigVariableType, {
|
||||
name: 'ConfigVariableType',
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
export class ConfigVariable {
|
||||
@ObjectType('ConfigVariable')
|
||||
export class ConfigVariableDTO {
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
|
||||
+5
-5
@@ -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;
|
||||
|
||||
+4
-4
@@ -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[];
|
||||
}
|
||||
|
||||
+12
@@ -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[];
|
||||
}
|
||||
+4
-4
@@ -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;
|
||||
}
|
||||
|
||||
+13
@@ -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[];
|
||||
}
|
||||
+22
@@ -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;
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class QueueMetricsDataPoint {
|
||||
@ObjectType('QueueMetricsDataPoint')
|
||||
export class QueueMetricsDataPointDTO {
|
||||
@Field(() => Number)
|
||||
x: number;
|
||||
|
||||
|
||||
+5
-5
@@ -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[];
|
||||
}
|
||||
|
||||
+5
-5
@@ -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[];
|
||||
}
|
||||
|
||||
+16
@@ -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;
|
||||
}
|
||||
+12
@@ -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[];
|
||||
}
|
||||
+6
-6
@@ -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[];
|
||||
}
|
||||
|
||||
+14
-14
@@ -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,
|
||||
};
|
||||
+9
-9
@@ -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>;
|
||||
|
||||
+4
-4
@@ -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;
|
||||
}
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||
|
||||
@ObjectType()
|
||||
@ObjectType('BillingPriceLicensed')
|
||||
export class BillingPriceLicensedDTO {
|
||||
@Field(() => SubscriptionInterval)
|
||||
recurringInterval: SubscriptionInterval;
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { BillingPriceTierDTO } from 'src/engine/core-modules/billing/dtos/billin
|
||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||
|
||||
@ObjectType()
|
||||
@ObjectType('BillingPriceMetered')
|
||||
export class BillingPriceMeteredDTO {
|
||||
@Field(() => [BillingPriceTierDTO])
|
||||
tiers: BillingPriceTierDTO[];
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
@ObjectType('BillingPriceTier')
|
||||
export class BillingPriceTierDTO {
|
||||
@Field(() => Number, { nullable: true })
|
||||
upTo: number | null;
|
||||
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class BillingSubscriptionSchedulePhaseItem {
|
||||
@ObjectType('BillingSubscriptionSchedulePhaseItem')
|
||||
export class BillingSubscriptionSchedulePhaseItemDTO {
|
||||
@Field(() => String)
|
||||
price: string;
|
||||
|
||||
@@ -9,14 +9,14 @@ export class BillingSubscriptionSchedulePhaseItem {
|
||||
quantity?: number;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class BillingSubscriptionSchedulePhase {
|
||||
@ObjectType('BillingSubscriptionSchedulePhase')
|
||||
export class BillingSubscriptionSchedulePhaseDTO {
|
||||
@Field(() => Number)
|
||||
start_date: number;
|
||||
|
||||
@Field(() => Number)
|
||||
end_date: number;
|
||||
|
||||
@Field(() => [BillingSubscriptionSchedulePhaseItem])
|
||||
items: Array<BillingSubscriptionSchedulePhaseItem>;
|
||||
@Field(() => [BillingSubscriptionSchedulePhaseItemDTO])
|
||||
items: Array<BillingSubscriptionSchedulePhaseItemDTO>;
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { Min } from 'class-validator';
|
||||
|
||||
@ObjectType()
|
||||
@ObjectType('BillingTrialPeriod')
|
||||
export class BillingTrialPeriodDTO {
|
||||
@Field(() => Number)
|
||||
@Min(0)
|
||||
|
||||
+4
-4
@@ -3,6 +3,7 @@
|
||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import graphqlTypeJson from 'graphql-type-json';
|
||||
import Stripe from 'stripe';
|
||||
import {
|
||||
Column,
|
||||
@@ -16,16 +17,15 @@ import {
|
||||
Relation,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import graphqlTypeJson from 'graphql-type-json';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { BillingSubscriptionSchedulePhaseDTO } from 'src/engine/core-modules/billing/dtos/billing-subscription-schedule-phase.dto';
|
||||
import { BillingSubscriptionItemDTO } from 'src/engine/core-modules/billing/dtos/outputs/billing-subscription-item.output';
|
||||
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||
import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
|
||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||
import { BillingSubscriptionSchedulePhase } from 'src/engine/core-modules/billing/dtos/billing-subscription-schedule-phase.dto';
|
||||
|
||||
registerEnumType(SubscriptionStatus, { name: 'SubscriptionStatus' });
|
||||
registerEnumType(SubscriptionInterval, { name: 'SubscriptionInterval' });
|
||||
@@ -122,9 +122,9 @@ export class BillingSubscription {
|
||||
@Column({ nullable: false, type: 'jsonb', default: {} })
|
||||
metadata: Stripe.Metadata;
|
||||
|
||||
@Field(() => [BillingSubscriptionSchedulePhase])
|
||||
@Field(() => [BillingSubscriptionSchedulePhaseDTO])
|
||||
@Column({ nullable: false, type: 'jsonb', default: [] })
|
||||
phases: Array<BillingSubscriptionSchedulePhase>;
|
||||
phases: Array<BillingSubscriptionSchedulePhaseDTO>;
|
||||
|
||||
@Column({ nullable: true, type: 'timestamptz' })
|
||||
cancelAt: Date | null;
|
||||
|
||||
+5
-5
@@ -3,21 +3,21 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
import {
|
||||
assertIsDefinedOrThrow,
|
||||
findOrThrow,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { BillingSubscriptionSchedulePhase } from 'src/engine/core-modules/billing/dtos/billing-subscription-schedule-phase.dto';
|
||||
import { BillingSubscriptionSchedulePhaseDTO } from 'src/engine/core-modules/billing/dtos/billing-subscription-schedule-phase.dto';
|
||||
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
|
||||
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
|
||||
import { normalizePriceRef } from 'src/engine/core-modules/billing/utils/normalize-price-ref.utils';
|
||||
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
|
||||
import { BillingPriceService } from 'src/engine/core-modules/billing/services/billing-price.service';
|
||||
import { normalizePriceRef } from 'src/engine/core-modules/billing/utils/normalize-price-ref.utils';
|
||||
|
||||
@Injectable()
|
||||
export class BillingSubscriptionPhaseService {
|
||||
@@ -28,7 +28,7 @@ export class BillingSubscriptionPhaseService {
|
||||
private readonly billingPriceService: BillingPriceService,
|
||||
) {}
|
||||
|
||||
async getDetailsFromPhase(phase: BillingSubscriptionSchedulePhase) {
|
||||
async getDetailsFromPhase(phase: BillingSubscriptionSchedulePhaseDTO) {
|
||||
const meteredPrice = await this.billingPriceRepository.findOneOrFail({
|
||||
where: {
|
||||
stripePriceId: findOrThrow(
|
||||
|
||||
+35
-35
@@ -3,56 +3,56 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Not, Repository } from 'typeorm';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import {
|
||||
assertIsDefinedOrThrow,
|
||||
findOrThrow,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { Not, Repository } from 'typeorm';
|
||||
|
||||
import type Stripe from 'stripe';
|
||||
|
||||
import { transformStripeSubscriptionEventToDatabaseCustomer } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-customer.util';
|
||||
import { transformStripeSubscriptionEventToDatabaseSubscriptionItem } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-subscription-item.util';
|
||||
import {
|
||||
getSubscriptionStatus,
|
||||
transformStripeSubscriptionEventToDatabaseSubscription,
|
||||
} from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-subscription.util';
|
||||
import {
|
||||
BillingException,
|
||||
BillingExceptionCode,
|
||||
} from 'src/engine/core-modules/billing/billing.exception';
|
||||
import { billingValidator } from 'src/engine/core-modules/billing/billing.validate';
|
||||
import { BillingSubscriptionSchedulePhaseDTO } from 'src/engine/core-modules/billing/dtos/billing-subscription-schedule-phase.dto';
|
||||
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
|
||||
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
|
||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { type BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||
import { BillingProductKey } from 'src/engine/core-modules/billing/enums/billing-product-key.enum';
|
||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
|
||||
import { BillingPriceService } from 'src/engine/core-modules/billing/services/billing-price.service';
|
||||
import { BillingProductService } from 'src/engine/core-modules/billing/services/billing-product.service';
|
||||
import { BillingSubscriptionPhaseService } from 'src/engine/core-modules/billing/services/billing-subscription-phase.service';
|
||||
import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/services/stripe-customer.service';
|
||||
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
|
||||
import { StripeSubscriptionScheduleService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-schedule.service';
|
||||
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
|
||||
import { BillingMeterPrice } from 'src/engine/core-modules/billing/types/billing-meter-price.type';
|
||||
import { LicensedBillingSubscriptionItem } from 'src/engine/core-modules/billing/types/billing-subscription-item.type';
|
||||
import { SubscriptionWithSchedule } from 'src/engine/core-modules/billing/types/billing-subscription-with-schedule.type';
|
||||
import { MeterBillingPriceTiers } from 'src/engine/core-modules/billing/types/meter-billing-price-tier.type';
|
||||
import { ensureFutureStartDate } from 'src/engine/core-modules/billing/utils/ensure-future-start-date.util';
|
||||
import { getOppositeInterval } from 'src/engine/core-modules/billing/utils/get-opposite-interval';
|
||||
import { getOppositePlan } from 'src/engine/core-modules/billing/utils/get-opposite-plan';
|
||||
import { getPlanKeyFromSubscription } from 'src/engine/core-modules/billing/utils/get-plan-key-from-subscription.util';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||
import { LicensedBillingSubscriptionItem } from 'src/engine/core-modules/billing/types/billing-subscription-item.type';
|
||||
import { BillingSubscriptionPhaseService } from 'src/engine/core-modules/billing/services/billing-subscription-phase.service';
|
||||
import { BillingSubscriptionSchedulePhase } from 'src/engine/core-modules/billing/dtos/billing-subscription-schedule-phase.dto';
|
||||
import { getOppositeInterval } from 'src/engine/core-modules/billing/utils/get-opposite-interval';
|
||||
import { billingValidator } from 'src/engine/core-modules/billing/billing.validate';
|
||||
import { MeterBillingPriceTiers } from 'src/engine/core-modules/billing/types/meter-billing-price-tier.type';
|
||||
import { BillingMeterPrice } from 'src/engine/core-modules/billing/types/billing-meter-price.type';
|
||||
import { BillingProductKey } from 'src/engine/core-modules/billing/enums/billing-product-key.enum';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { getOppositePlan } from 'src/engine/core-modules/billing/utils/get-opposite-plan';
|
||||
import {
|
||||
getSubscriptionStatus,
|
||||
transformStripeSubscriptionEventToDatabaseSubscription,
|
||||
} from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-subscription.util';
|
||||
import { transformStripeSubscriptionEventToDatabaseSubscriptionItem } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-subscription-item.util';
|
||||
import { transformStripeSubscriptionEventToDatabaseCustomer } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-customer.util';
|
||||
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||
import { SubscriptionWithSchedule } from 'src/engine/core-modules/billing/types/billing-subscription-with-schedule.type';
|
||||
import { ensureFutureStartDate } from 'src/engine/core-modules/billing/utils/ensure-future-start-date.util';
|
||||
import { BillingPriceService } from 'src/engine/core-modules/billing/services/billing-price.service';
|
||||
|
||||
@Injectable()
|
||||
export class BillingSubscriptionService {
|
||||
@@ -287,7 +287,7 @@ export class BillingSubscriptionService {
|
||||
|
||||
const currentPhaseDetails =
|
||||
await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
currentEditable as BillingSubscriptionSchedulePhase,
|
||||
currentEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
);
|
||||
|
||||
await this.changeMeteredPrice(
|
||||
@@ -634,12 +634,12 @@ export class BillingSubscriptionService {
|
||||
}
|
||||
const currentPhaseDetails =
|
||||
await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
currentEditable as BillingSubscriptionSchedulePhase,
|
||||
currentEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
);
|
||||
const hasNextInitially = !!nextEditable;
|
||||
const nextPhaseDetailsInitial = hasNextInitially
|
||||
? await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
nextEditable as BillingSubscriptionSchedulePhase,
|
||||
nextEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
)
|
||||
: undefined;
|
||||
const currentCap = (currentPhaseDetails.meteredPrice as BillingMeterPrice)
|
||||
@@ -734,7 +734,7 @@ export class BillingSubscriptionService {
|
||||
const hasNext = !!nextEditable;
|
||||
const nextPhaseDetails = hasNext
|
||||
? await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
nextEditable as BillingSubscriptionSchedulePhase,
|
||||
nextEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
)
|
||||
: undefined;
|
||||
const currentLicensedId = currentSnap
|
||||
@@ -906,7 +906,7 @@ export class BillingSubscriptionService {
|
||||
|
||||
const currentDetails =
|
||||
await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
currentEditable as BillingSubscriptionSchedulePhase,
|
||||
currentEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
);
|
||||
const { nextEditable } = await this.loadScheduleEditable(
|
||||
billingSubscription.stripeSubscriptionId,
|
||||
@@ -925,7 +925,7 @@ export class BillingSubscriptionService {
|
||||
|
||||
const nextDetails =
|
||||
await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
nextEditable as BillingSubscriptionSchedulePhase,
|
||||
nextEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
);
|
||||
|
||||
if (nextDetails.interval !== targetInterval) {
|
||||
@@ -1000,7 +1000,7 @@ export class BillingSubscriptionService {
|
||||
if (nextEditable && currentEditable) {
|
||||
const reloadedNextDetails =
|
||||
await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
nextEditable as BillingSubscriptionSchedulePhase,
|
||||
nextEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
);
|
||||
|
||||
const mappedNext = await this.resolvePrices({
|
||||
@@ -1048,7 +1048,7 @@ export class BillingSubscriptionService {
|
||||
|
||||
const nextDetails = hasNext
|
||||
? await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
nextEditable as BillingSubscriptionSchedulePhase,
|
||||
nextEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
@@ -1099,7 +1099,7 @@ export class BillingSubscriptionService {
|
||||
|
||||
const currentDetails =
|
||||
await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
currentEditable as BillingSubscriptionSchedulePhase,
|
||||
currentEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
);
|
||||
|
||||
const currentPlan = currentDetails.plan.planKey;
|
||||
@@ -1115,7 +1115,7 @@ export class BillingSubscriptionService {
|
||||
|
||||
const nextDetails =
|
||||
await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
nextEditable as BillingSubscriptionSchedulePhase,
|
||||
nextEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
);
|
||||
|
||||
if (nextDetails.plan.planKey !== targetPlanKey) {
|
||||
@@ -1181,7 +1181,7 @@ export class BillingSubscriptionService {
|
||||
if (nextEditable && currentEditable) {
|
||||
const nextDetails =
|
||||
await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
nextEditable as BillingSubscriptionSchedulePhase,
|
||||
nextEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
);
|
||||
|
||||
const preservedNextInterval = nextDetails?.interval ?? interval;
|
||||
@@ -1233,7 +1233,7 @@ export class BillingSubscriptionService {
|
||||
|
||||
const nextDetails = hasNext
|
||||
? await this.billingSubscriptionPhaseService.getDetailsFromPhase(
|
||||
nextEditable as BillingSubscriptionSchedulePhase,
|
||||
nextEditable as BillingSubscriptionSchedulePhaseDTO,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
|
||||
+13
-13
@@ -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;
|
||||
|
||||
+5
-5
@@ -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
-5
@@ -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()
|
||||
{
|
||||
|
||||
+4
-4
@@ -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',
|
||||
|
||||
+3
-3
@@ -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}`;
|
||||
|
||||
+4
-4
@@ -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}`,
|
||||
|
||||
+1
-1
@@ -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}`;
|
||||
|
||||
+3
-3
@@ -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;
|
||||
|
||||
+2
-2
@@ -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';
|
||||
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -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
Reference in New Issue
Block a user