Add server upgrade status on admin panel (#20107)
## Summary Adds an admin upgrade-status panel that surfaces per-instance and per-workspace migration health, backed by a Redis-cached aggregate to keep the page snappy on large fleets. <img width="827" height="880" alt="Screenshot 2026-04-28 at 10 21 03" src="https://github.com/user-attachments/assets/8f88baa9-7268-4eff-bf6a-906a7f06ca91" /> <img width="804" height="892" alt="Screenshot 2026-04-28 at 10 21 11" src="https://github.com/user-attachments/assets/1e6decf8-766a-4d0e-96b1-03a9962bba3c" /> ## Computed metrics **Instance** (`InstanceUpgradeStatus`) - `inferredVersion` — version derived from the latest non-initial instance command name - `health` — `upToDate` | `behind` | `failed`, derived from the latest attempt vs. the last expected instance step in the upgrade sequence - `latestCommand` — `{ name, status, executedByVersion, errorMessage, createdAt }` from the most recent attempt **Per-workspace** (`WorkspaceUpgradeStatus`) - `workspaceId`, `displayName` - `inferredVersion`, `health`, `latestCommand` (same shape as instance), computed against the latest expected step in the sequence **Aggregate** (`AllWorkspacesUpgradeStatus`, only across `ACTIVE` / `SUSPENDED` workspaces) - `instanceUpgradeStatus` - `totalCount`, `upToDateCount`, `behindCount`, `failedCount` - `workspacesBehindIds[]`, `workspacesFailedIds[]` - `computedAt` ## Fetching strategy All reads go through `UpgradeStatusCacheService` (cache namespace: `EngineHealth`). - **Aggregate read** (`getAllWorkspacesStatus` → `getAllWorkspacesUpgradeStatus` query): reads summary + behind-ids + failed-ids in parallel; if any of the three keys is missing, full recompute (`recomputeAllWorkspaces`) is triggered, which also primes per-workspace entries. - **Per-workspace read** (`getWorkspacesStatus(ids)` → `getUpgradeStatus(ids)` query): `mget` on workspace keys; misses are recomputed individually (`recomputeWorkspace`), and aggregates are reconciled in place (count + id list deltas) without a full recompute. - **Recompute on demand**: `refreshUpgradeStatus` mutation calls `recomputeAllWorkspaces` to bypass cache and rewrite all keys. - **Auto-invalidation**: `InstanceCommandRunnerService` (fast + slow paths) and `WorkspaceCommandRunnerService` invalidate after every run via `safeInvalidateUpgradeStatusCache()` (`flushByPattern('upgrade-status:*')`). Failures in cache invalidation are swallowed and logged so they never break the migration runner. - **TTL**: `60 * 60 * 1000` ms (1 hour) on every key — protects against stale data even if a runner crashes before invalidating. ## Introduced cache keys All under the `EngineHealth` cache-storage namespace: | Key | Type | Purpose | | --- | --- | --- | | `upgrade-status:all-workspaces:summary` | `CachedAllWorkspacesStatusSummary` | Counts + instance status + `computedAt` | | `upgrade-status:all-workspaces:behind-ids` | `string[]` | Workspace ids in `behind` state | | `upgrade-status:all-workspaces:failed-ids` | `string[]` | Workspace ids in `failed` state | | `upgrade-status:workspace:<workspaceId>` | `CachedWorkspaceUpgradeStatus` | Per-workspace status (one key per workspace) | Full invalidation uses the pattern `upgrade-status:*`. ## Index added on `upgradeMigration` (already added on prod) Migration `2-2-instance-command-fast-1777308014234-addUpgradeMigrationWorkspaceIdIndex.ts`: ```sql CREATE INDEX "IDX_upgradeMigration_workspaceId_name_attempt" ON "core"."upgradeMigration" ("workspaceId", "name", "attempt") WHERE "workspaceId" IS NOT NULL;
This commit is contained in:
@@ -291,6 +291,21 @@ export enum HealthIndicatorId {
|
||||
worker = 'worker'
|
||||
}
|
||||
|
||||
export type InstanceAndAllWorkspacesUpgradeStatus = {
|
||||
__typename?: 'InstanceAndAllWorkspacesUpgradeStatus';
|
||||
computedAt: Scalars['DateTime'];
|
||||
instanceUpgradeStatus: InstanceUpgradeStatus;
|
||||
workspacesBehind: Array<WorkspaceUpgradeRef>;
|
||||
workspacesFailed: Array<WorkspaceUpgradeRef>;
|
||||
};
|
||||
|
||||
export type InstanceUpgradeStatus = {
|
||||
__typename?: 'InstanceUpgradeStatus';
|
||||
health: UpgradeHealth;
|
||||
inferredVersion?: Maybe<Scalars['String']>;
|
||||
latestCommand?: Maybe<LatestUpgradeCommand>;
|
||||
};
|
||||
|
||||
export type JobOperationResult = {
|
||||
__typename?: 'JobOperationResult';
|
||||
error?: Maybe<Scalars['String']>;
|
||||
@@ -309,6 +324,15 @@ export enum JobState {
|
||||
WAITING_CHILDREN = 'WAITING_CHILDREN'
|
||||
}
|
||||
|
||||
export type LatestUpgradeCommand = {
|
||||
__typename?: 'LatestUpgradeCommand';
|
||||
createdAt: Scalars['DateTime'];
|
||||
errorMessage?: Maybe<Scalars['String']>;
|
||||
executedByVersion: Scalars['String'];
|
||||
name: Scalars['String'];
|
||||
status: Scalars['String'];
|
||||
};
|
||||
|
||||
export type MaintenanceMode = {
|
||||
__typename?: 'MaintenanceMode';
|
||||
endAt: Scalars['DateTime'];
|
||||
@@ -353,6 +377,7 @@ export type Mutation = {
|
||||
createDatabaseConfigVariable: Scalars['Boolean'];
|
||||
deleteDatabaseConfigVariable: Scalars['Boolean'];
|
||||
deleteJobs: DeleteJobsResponse;
|
||||
refreshUpgradeStatus: InstanceAndAllWorkspacesUpgradeStatus;
|
||||
removeAiProvider: Scalars['Boolean'];
|
||||
removeModelFromProvider: Scalars['Boolean'];
|
||||
retryJobs: RetryJobsResponse;
|
||||
@@ -476,12 +501,14 @@ export type Query = {
|
||||
getConfigVariablesGrouped: ConfigVariables;
|
||||
getDatabaseConfigVariable: ConfigVariable;
|
||||
getIndicatorHealthStatus: AdminPanelHealthServiceData;
|
||||
getInstanceAndAllWorkspacesUpgradeStatus: InstanceAndAllWorkspacesUpgradeStatus;
|
||||
getMaintenanceMode?: Maybe<MaintenanceMode>;
|
||||
getModelsDevProviders: Array<ModelsDevProviderSuggestion>;
|
||||
getModelsDevSuggestions: Array<ModelsDevModelSuggestion>;
|
||||
getQueueJobs: QueueJobsResponse;
|
||||
getQueueMetrics: QueueMetricsData;
|
||||
getSystemHealthStatus: SystemHealth;
|
||||
getUpgradeStatus: Array<WorkspaceUpgradeStatus>;
|
||||
userLookupAdminPanel: UserLookup;
|
||||
versionInfo: VersionInfo;
|
||||
workspaceBillingAdminPanel?: Maybe<AdminPanelWorkspaceBilling>;
|
||||
@@ -549,6 +576,11 @@ export type QueryGetQueueMetricsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetUpgradeStatusArgs = {
|
||||
workspaceIds: Array<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryUserLookupAdminPanelArgs = {
|
||||
userIdentifier: Scalars['String'];
|
||||
};
|
||||
@@ -659,6 +691,12 @@ export type SystemHealthService = {
|
||||
status: AdminPanelHealthServiceStatus;
|
||||
};
|
||||
|
||||
export enum UpgradeHealth {
|
||||
BEHIND = 'BEHIND',
|
||||
FAILED = 'FAILED',
|
||||
UP_TO_DATE = 'UP_TO_DATE'
|
||||
}
|
||||
|
||||
export type UsageBreakdownItem = {
|
||||
__typename?: 'UsageBreakdownItem';
|
||||
creditsUsed: Scalars['Float'];
|
||||
@@ -678,7 +716,7 @@ export type UserInfo = {
|
||||
|
||||
export type UserLookup = {
|
||||
__typename?: 'UserLookup';
|
||||
user: UserInfo;
|
||||
user?: Maybe<UserInfo>;
|
||||
workspaces: Array<WorkspaceInfo>;
|
||||
};
|
||||
|
||||
@@ -722,6 +760,21 @@ export type WorkspaceInfo = {
|
||||
workspaceUrls: WorkspaceUrls;
|
||||
};
|
||||
|
||||
export type WorkspaceUpgradeRef = {
|
||||
__typename?: 'WorkspaceUpgradeRef';
|
||||
id: Scalars['UUID'];
|
||||
name?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type WorkspaceUpgradeStatus = {
|
||||
__typename?: 'WorkspaceUpgradeStatus';
|
||||
displayName?: Maybe<Scalars['String']>;
|
||||
health: UpgradeHealth;
|
||||
inferredVersion?: Maybe<Scalars['String']>;
|
||||
latestCommand?: Maybe<LatestUpgradeCommand>;
|
||||
workspaceId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type WorkspaceUrls = {
|
||||
__typename?: 'WorkspaceUrls';
|
||||
customUrl?: Maybe<Scalars['String']>;
|
||||
@@ -915,6 +968,13 @@ export type GetAdminWorkspaceChatThreadsQueryVariables = Exact<{
|
||||
|
||||
export type GetAdminWorkspaceChatThreadsQuery = { __typename?: 'Query', getAdminWorkspaceChatThreads: Array<{ __typename?: 'AdminWorkspaceChatThread', id: string, title?: string | null, totalInputTokens: number, totalOutputTokens: number, conversationSize: number, createdAt: string, updatedAt: string }> };
|
||||
|
||||
export type GetUpgradeStatusQueryVariables = Exact<{
|
||||
workspaceIds: Array<Scalars['UUID']> | Scalars['UUID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetUpgradeStatusQuery = { __typename?: 'Query', getUpgradeStatus: Array<{ __typename?: 'WorkspaceUpgradeStatus', workspaceId: string, displayName?: string | null, inferredVersion?: string | null, health: UpgradeHealth, latestCommand?: { __typename?: 'LatestUpgradeCommand', name: string, status: string, executedByVersion: string, errorMessage?: string | null, createdAt: string } | null }> };
|
||||
|
||||
export type GetVersionInfoQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
@@ -932,14 +992,14 @@ export type UserLookupAdminPanelQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type UserLookupAdminPanelQuery = { __typename?: 'Query', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null, createdAt: string }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, activationStatus: WorkspaceActivationStatus, createdAt: string, allowImpersonation: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', customUrl?: string | null, subdomainUrl: string }, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> }> } };
|
||||
export type UserLookupAdminPanelQuery = { __typename?: 'Query', userLookupAdminPanel: { __typename?: 'UserLookup', user?: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null, createdAt: string } | null, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, activationStatus: WorkspaceActivationStatus, createdAt: string, allowImpersonation: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', customUrl?: string | null, subdomainUrl: string }, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> }> } };
|
||||
|
||||
export type WorkspaceLookupAdminPanelQueryVariables = Exact<{
|
||||
workspaceId: Scalars['UUID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type WorkspaceLookupAdminPanelQuery = { __typename?: 'Query', workspaceLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null, createdAt: string }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, allowImpersonation: boolean, logo?: string | null, totalUsers: number, activationStatus: WorkspaceActivationStatus, createdAt: string, workspaceUrls: { __typename?: 'WorkspaceUrls', customUrl?: string | null, subdomainUrl: string }, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> }> } };
|
||||
export type WorkspaceLookupAdminPanelQuery = { __typename?: 'Query', workspaceLookupAdminPanel: { __typename?: 'UserLookup', user?: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null, createdAt: string } | null, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, allowImpersonation: boolean, logo?: string | null, totalUsers: number, activationStatus: WorkspaceActivationStatus, createdAt: string, workspaceUrls: { __typename?: 'WorkspaceUrls', customUrl?: string | null, subdomainUrl: string }, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> }> } };
|
||||
|
||||
export type DeleteJobsMutationVariables = Exact<{
|
||||
queueName: Scalars['String'];
|
||||
@@ -949,6 +1009,11 @@ export type DeleteJobsMutationVariables = Exact<{
|
||||
|
||||
export type DeleteJobsMutation = { __typename?: 'Mutation', deleteJobs: { __typename?: 'DeleteJobsResponse', deletedCount: number, results: Array<{ __typename?: 'JobOperationResult', jobId: string, success: boolean, error?: string | null }> } };
|
||||
|
||||
export type RefreshUpgradeStatusMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type RefreshUpgradeStatusMutation = { __typename?: 'Mutation', refreshUpgradeStatus: { __typename?: 'InstanceAndAllWorkspacesUpgradeStatus', computedAt: string, instanceUpgradeStatus: { __typename?: 'InstanceUpgradeStatus', inferredVersion?: string | null, health: UpgradeHealth, latestCommand?: { __typename?: 'LatestUpgradeCommand', name: string, status: string, executedByVersion: string, errorMessage?: string | null, createdAt: string } | null }, workspacesBehind: Array<{ __typename?: 'WorkspaceUpgradeRef', id: string, name?: string | null }>, workspacesFailed: Array<{ __typename?: 'WorkspaceUpgradeRef', id: string, name?: string | null }> } };
|
||||
|
||||
export type RetryJobsMutationVariables = Exact<{
|
||||
queueName: Scalars['String'];
|
||||
jobIds: Array<Scalars['String']> | Scalars['String'];
|
||||
@@ -964,6 +1029,11 @@ 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 GetInstanceAndAllWorkspacesUpgradeStatusQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetInstanceAndAllWorkspacesUpgradeStatusQuery = { __typename?: 'Query', getInstanceAndAllWorkspacesUpgradeStatus: { __typename?: 'InstanceAndAllWorkspacesUpgradeStatus', computedAt: string, instanceUpgradeStatus: { __typename?: 'InstanceUpgradeStatus', inferredVersion?: string | null, health: UpgradeHealth, latestCommand?: { __typename?: 'LatestUpgradeCommand', name: string, status: string, executedByVersion: string, errorMessage?: string | null, createdAt: string } | null }, workspacesBehind: Array<{ __typename?: 'WorkspaceUpgradeRef', id: string, name?: string | null }>, workspacesFailed: Array<{ __typename?: 'WorkspaceUpgradeRef', id: string, name?: string | null }> } };
|
||||
|
||||
export type GetQueueJobsQueryVariables = Exact<{
|
||||
queueName: Scalars['String'];
|
||||
state: JobState;
|
||||
@@ -1036,13 +1106,16 @@ export const AdminPanelTopWorkspacesDocument = {"kind":"Document","definitions":
|
||||
export const FindOneAdminApplicationRegistrationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneAdminApplicationRegistration"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findOneAdminApplicationRegistration"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ApplicationRegistrationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApplicationRegistrationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApplicationRegistration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthClientId"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthRedirectUris"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthScopes"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"isPreInstalled"}},{"kind":"Field","name":{"kind":"Name","value":"ownerWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<FindOneAdminApplicationRegistrationQuery, FindOneAdminApplicationRegistrationQueryVariables>;
|
||||
export const GetAdminChatThreadMessagesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAdminChatThreadMessages"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAdminChatThreadMessages"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"thread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"totalInputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"totalOutputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"conversationSize"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"messages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"parts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"textContent"}},{"kind":"Field","name":{"kind":"Name","value":"toolName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode<GetAdminChatThreadMessagesQuery, GetAdminChatThreadMessagesQueryVariables>;
|
||||
export const GetAdminWorkspaceChatThreadsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAdminWorkspaceChatThreads"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAdminWorkspaceChatThreads"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"totalInputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"totalOutputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"conversationSize"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<GetAdminWorkspaceChatThreadsQuery, GetAdminWorkspaceChatThreadsQueryVariables>;
|
||||
export const GetUpgradeStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUpgradeStatus"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getUpgradeStatus"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"inferredVersion"}},{"kind":"Field","name":{"kind":"Name","value":"health"}},{"kind":"Field","name":{"kind":"Name","value":"latestCommand"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"executedByVersion"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode<GetUpgradeStatusQuery, GetUpgradeStatusQueryVariables>;
|
||||
export const GetVersionInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetVersionInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"versionInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currentVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}}]}}]}}]} as unknown as DocumentNode<GetVersionInfoQuery, GetVersionInfoQueryVariables>;
|
||||
export const WorkspaceBillingAdminPanelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"WorkspaceBillingAdminPanel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceBillingAdminPanel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stripeCustomerId"}},{"kind":"Field","name":{"kind":"Name","value":"creditBalance"}},{"kind":"Field","name":{"kind":"Name","value":"subscription"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stripeSubscriptionId"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"interval"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"planKey"}},{"kind":"Field","name":{"kind":"Name","value":"currentPeriodStart"}},{"kind":"Field","name":{"kind":"Name","value":"currentPeriodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"trialStart"}},{"kind":"Field","name":{"kind":"Name","value":"trialEnd"}},{"kind":"Field","name":{"kind":"Name","value":"cancelAt"}},{"kind":"Field","name":{"kind":"Name","value":"canceledAt"}},{"kind":"Field","name":{"kind":"Name","value":"cancelAtPeriodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"productName"}},{"kind":"Field","name":{"kind":"Name","value":"productKey"}},{"kind":"Field","name":{"kind":"Name","value":"stripePriceId"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"unitAmount"}},{"kind":"Field","name":{"kind":"Name","value":"includedCredits"}}]}}]}}]}}]}}]} as unknown as DocumentNode<WorkspaceBillingAdminPanelQuery, WorkspaceBillingAdminPanelQueryVariables>;
|
||||
export const UserLookupAdminPanelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserLookupAdminPanel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userLookupAdminPanel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userIdentifier"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserInfoFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"totalUsers"}},{"kind":"Field","name":{"kind":"Name","value":"activationStatus"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"allowImpersonation"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customUrl"}},{"kind":"Field","name":{"kind":"Name","value":"subdomainUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featureFlags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserInfoFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"UserInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode<UserLookupAdminPanelQuery, UserLookupAdminPanelQueryVariables>;
|
||||
export const WorkspaceLookupAdminPanelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"WorkspaceLookupAdminPanel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceLookupAdminPanel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserInfoFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"allowImpersonation"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"totalUsers"}},{"kind":"Field","name":{"kind":"Name","value":"activationStatus"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customUrl"}},{"kind":"Field","name":{"kind":"Name","value":"subdomainUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featureFlags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserInfoFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"UserInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode<WorkspaceLookupAdminPanelQuery, WorkspaceLookupAdminPanelQueryVariables>;
|
||||
export const DeleteJobsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteJobs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"jobIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteJobs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"queueName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}}},{"kind":"Argument","name":{"kind":"Name","value":"jobIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"jobIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deletedCount"}},{"kind":"Field","name":{"kind":"Name","value":"results"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jobId"}},{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]}}]} as unknown as DocumentNode<DeleteJobsMutation, DeleteJobsMutationVariables>;
|
||||
export const RefreshUpgradeStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RefreshUpgradeStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"refreshUpgradeStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"instanceUpgradeStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inferredVersion"}},{"kind":"Field","name":{"kind":"Name","value":"health"}},{"kind":"Field","name":{"kind":"Name","value":"latestCommand"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"executedByVersion"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspacesBehind"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspacesFailed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"computedAt"}}]}}]}}]} as unknown as DocumentNode<RefreshUpgradeStatusMutation, RefreshUpgradeStatusMutationVariables>;
|
||||
export const RetryJobsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RetryJobs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"jobIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"retryJobs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"queueName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}}},{"kind":"Argument","name":{"kind":"Name","value":"jobIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"jobIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"retriedCount"}},{"kind":"Field","name":{"kind":"Name","value":"results"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jobId"}},{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]}}]} as unknown as DocumentNode<RetryJobsMutation, RetryJobsMutationVariables>;
|
||||
export const GetIndicatorHealthStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetIndicatorHealthStatus"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"indicatorId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"HealthIndicatorId"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getIndicatorHealthStatus"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"indicatorId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"indicatorId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}},{"kind":"Field","name":{"kind":"Name","value":"details"}},{"kind":"Field","name":{"kind":"Name","value":"queues"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"queueName"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode<GetIndicatorHealthStatusQuery, GetIndicatorHealthStatusQueryVariables>;
|
||||
export const GetInstanceAndAllWorkspacesUpgradeStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetInstanceAndAllWorkspacesUpgradeStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getInstanceAndAllWorkspacesUpgradeStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"instanceUpgradeStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inferredVersion"}},{"kind":"Field","name":{"kind":"Name","value":"health"}},{"kind":"Field","name":{"kind":"Name","value":"latestCommand"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"executedByVersion"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspacesBehind"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspacesFailed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"computedAt"}}]}}]}}]} as unknown as DocumentNode<GetInstanceAndAllWorkspacesUpgradeStatusQuery, GetInstanceAndAllWorkspacesUpgradeStatusQueryVariables>;
|
||||
export const GetQueueJobsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetQueueJobs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"state"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"JobState"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"offset"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getQueueJobs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"queueName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}}},{"kind":"Argument","name":{"kind":"Name","value":"state"},"value":{"kind":"Variable","name":{"kind":"Name","value":"state"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"offset"},"value":{"kind":"Variable","name":{"kind":"Name","value":"offset"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jobs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"failedReason"}},{"kind":"Field","name":{"kind":"Name","value":"processedOn"}},{"kind":"Field","name":{"kind":"Name","value":"finishedOn"}},{"kind":"Field","name":{"kind":"Name","value":"attemptsMade"}},{"kind":"Field","name":{"kind":"Name","value":"returnValue"}},{"kind":"Field","name":{"kind":"Name","value":"logs"}},{"kind":"Field","name":{"kind":"Name","value":"stackTrace"}}]}},{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"hasMore"}},{"kind":"Field","name":{"kind":"Name","value":"retentionConfig"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"completedMaxAge"}},{"kind":"Field","name":{"kind":"Name","value":"completedMaxCount"}},{"kind":"Field","name":{"kind":"Name","value":"failedMaxAge"}},{"kind":"Field","name":{"kind":"Name","value":"failedMaxCount"}}]}}]}}]}}]} as unknown as DocumentNode<GetQueueJobsQuery, GetQueueJobsQueryVariables>;
|
||||
export const GetQueueMetricsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetQueueMetrics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"timeRange"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"QueueMetricsTimeRange"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getQueueMetrics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"queueName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}}},{"kind":"Argument","name":{"kind":"Name","value":"timeRange"},"value":{"kind":"Variable","name":{"kind":"Name","value":"timeRange"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"queueName"}},{"kind":"Field","name":{"kind":"Name","value":"timeRange"}},{"kind":"Field","name":{"kind":"Name","value":"workers"}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"failed"}},{"kind":"Field","name":{"kind":"Name","value":"completed"}},{"kind":"Field","name":{"kind":"Name","value":"waiting"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"delayed"}},{"kind":"Field","name":{"kind":"Name","value":"failureRate"}}]}},{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"x"}},{"kind":"Field","name":{"kind":"Name","value":"y"}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetQueueMetricsQuery, GetQueueMetricsQueryVariables>;
|
||||
export const GetSystemHealthStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSystemHealthStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getSystemHealthStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"services"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode<GetSystemHealthStatusQuery, GetSystemHealthStatusQueryVariables>;
|
||||
|
||||
@@ -423,6 +423,30 @@ const SettingsAdminIndicatorHealthStatus = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsAdminInferredVersion = lazy(() =>
|
||||
import('~/pages/settings/admin-panel/SettingsAdminInferredVersion').then(
|
||||
(module) => ({
|
||||
default: module.SettingsAdminInferredVersion,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsAdminInstanceStatus = lazy(() =>
|
||||
import('~/pages/settings/admin-panel/SettingsAdminInstanceStatus').then(
|
||||
(module) => ({
|
||||
default: module.SettingsAdminInstanceStatus,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsAdminWorkspacesStatus = lazy(() =>
|
||||
import('~/pages/settings/admin-panel/SettingsAdminWorkspacesStatus').then(
|
||||
(module) => ({
|
||||
default: module.SettingsAdminWorkspacesStatus,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsAdminQueueDetail = lazy(() =>
|
||||
import('~/pages/settings/admin-panel/SettingsAdminQueueDetail').then(
|
||||
(module) => ({
|
||||
@@ -823,6 +847,18 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.AdminPanelInferredVersion}
|
||||
element={<SettingsAdminInferredVersion />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.AdminPanelInstanceStatus}
|
||||
element={<SettingsAdminInstanceStatus />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.AdminPanelWorkspacesStatus}
|
||||
element={<SettingsAdminWorkspacesStatus />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.AdminPanelIndicatorHealthStatus}
|
||||
element={<SettingsAdminIndicatorHealthStatus />}
|
||||
|
||||
+96
-1
@@ -1,16 +1,22 @@
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import { type WorkspaceInfo } from '@/settings/admin-panel/types/WorkspaceInfo';
|
||||
import { getUpgradeHealthStatusBadge } from '@/settings/admin-panel/utils/getUpgradeHealthStatusBadge';
|
||||
import { getWorkspaceSchemaName } from '@/settings/admin-panel/utils/getWorkspaceSchemaName';
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useContext } from 'react';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import {
|
||||
formatUpgradeCommandName,
|
||||
getImageAbsoluteURI,
|
||||
getSettingsPath,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { type GetUpgradeStatusQuery } from '~/generated-admin/graphql';
|
||||
import { AvatarOrIcon, LinkChip } from 'twenty-ui/components';
|
||||
import {
|
||||
H2Title,
|
||||
@@ -20,13 +26,18 @@ import {
|
||||
IconLink,
|
||||
IconStatusChange,
|
||||
IconUser,
|
||||
OverflowingTextWithTooltip,
|
||||
Status,
|
||||
} from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
|
||||
|
||||
type SettingsAdminWorkspaceContentProps = {
|
||||
activeWorkspace: WorkspaceInfo | undefined;
|
||||
workspaceUpgradeStatus?: GetUpgradeStatusQuery['getUpgradeStatus'][number];
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@@ -38,13 +49,28 @@ const StyledContainer = styled.div`
|
||||
|
||||
export const SettingsAdminWorkspaceContent = ({
|
||||
activeWorkspace,
|
||||
workspaceUpgradeStatus,
|
||||
}: SettingsAdminWorkspaceContentProps) => {
|
||||
const { t } = useLingui();
|
||||
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
|
||||
const { localeCatalog } = useAtomStateValue(dateLocaleState);
|
||||
|
||||
const formattedLastUpdated = formatDateTimeString({
|
||||
value: workspaceUpgradeStatus?.latestCommand?.createdAt,
|
||||
timeZone,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
localeCatalog: localeCatalog,
|
||||
});
|
||||
|
||||
const getWorkspaceUrl = (workspaceUrls: WorkspaceInfo['workspaceUrls']) => {
|
||||
return workspaceUrls.customUrl ?? workspaceUrls.subdomainUrl;
|
||||
};
|
||||
|
||||
const upgradeHealthStatusBadge = getUpgradeHealthStatusBadge(
|
||||
workspaceUpgradeStatus?.health,
|
||||
);
|
||||
|
||||
const workspaceInfoItems = [
|
||||
{
|
||||
Icon: IconHome,
|
||||
@@ -125,6 +151,75 @@ export const SettingsAdminWorkspaceContent = ({
|
||||
gridAutoColumns="1fr 4fr"
|
||||
/>
|
||||
</Section>
|
||||
{workspaceUpgradeStatus && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Upgrade Status`}
|
||||
description={t`Workspace upgrade health`}
|
||||
/>
|
||||
<SettingsTableCard
|
||||
items={[
|
||||
{
|
||||
Icon: IconStatusChange,
|
||||
label: t`Status`,
|
||||
value: (
|
||||
<Status
|
||||
color={upgradeHealthStatusBadge.color}
|
||||
text={upgradeHealthStatusBadge.label}
|
||||
weight="medium"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
Icon: IconId,
|
||||
label: t`Inferred version`,
|
||||
value: workspaceUpgradeStatus.inferredVersion ?? t`Unknown`,
|
||||
},
|
||||
{
|
||||
Icon: IconCalendar,
|
||||
label: t`Last command`,
|
||||
value: (
|
||||
<OverflowingTextWithTooltip
|
||||
text={
|
||||
workspaceUpgradeStatus.latestCommand?.name
|
||||
? formatUpgradeCommandName(
|
||||
workspaceUpgradeStatus.latestCommand.name,
|
||||
)
|
||||
: t`None`
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
Icon: IconCalendar,
|
||||
label: t`Last updated`,
|
||||
value: isNonEmptyString(formattedLastUpdated)
|
||||
? formattedLastUpdated
|
||||
: t`N/A`,
|
||||
},
|
||||
{
|
||||
Icon: IconStatusChange,
|
||||
label: t`Last command result`,
|
||||
value: workspaceUpgradeStatus.latestCommand?.status
|
||||
? workspaceUpgradeStatus.latestCommand.status === 'completed'
|
||||
? t`Completed`
|
||||
: t`Failed`
|
||||
: t`N/A`,
|
||||
},
|
||||
...(workspaceUpgradeStatus.latestCommand?.errorMessage
|
||||
? [
|
||||
{
|
||||
Icon: IconStatusChange,
|
||||
label: t`Last error`,
|
||||
value: workspaceUpgradeStatus.latestCommand.errorMessage,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
gridAutoColumns="2fr 3fr"
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_UPGRADE_STATUS = gql`
|
||||
query GetUpgradeStatus($workspaceIds: [UUID!]!) {
|
||||
getUpgradeStatus(workspaceIds: $workspaceIds) {
|
||||
workspaceId
|
||||
displayName
|
||||
inferredVersion
|
||||
health
|
||||
latestCommand {
|
||||
name
|
||||
status
|
||||
executedByVersion
|
||||
errorMessage
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
+27
-5
@@ -1,13 +1,17 @@
|
||||
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
|
||||
import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader';
|
||||
import { SettingsAdminHealthStatusListCard } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthStatusListCard';
|
||||
import { SettingsAdminMaintenanceModeFetchEffect } from '@/settings/admin-panel/health-status/maintenance-mode/components/SettingsAdminMaintenanceModeFetchEffect';
|
||||
import { SettingsAdminUpgradeStatusListCard } from '@/settings/admin-panel/health-status/components/SettingsAdminUpgradeStatusListCard';
|
||||
import { SettingsAdminMaintenanceMode } from '@/settings/admin-panel/health-status/maintenance-mode/components/SettingsAdminMaintenanceMode';
|
||||
import { SettingsAdminMaintenanceModeFetchEffect } from '@/settings/admin-panel/health-status/maintenance-mode/components/SettingsAdminMaintenanceModeFetchEffect';
|
||||
import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { GetSystemHealthStatusDocument } from '~/generated-admin/graphql';
|
||||
import {
|
||||
GetInstanceAndAllWorkspacesUpgradeStatusDocument,
|
||||
GetSystemHealthStatusDocument,
|
||||
} from '~/generated-admin/graphql';
|
||||
|
||||
export const SettingsAdminHealthStatus = () => {
|
||||
const apolloAdminClient = useApolloAdminClient();
|
||||
@@ -18,10 +22,19 @@ export const SettingsAdminHealthStatus = () => {
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
);
|
||||
const { data: upgradeStatusData, loading: loadingUpgradeStatus } = useQuery(
|
||||
GetInstanceAndAllWorkspacesUpgradeStatusDocument,
|
||||
{
|
||||
client: apolloAdminClient,
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
);
|
||||
|
||||
const services = data?.getSystemHealthStatus.services ?? [];
|
||||
const upgradeStatus =
|
||||
upgradeStatusData?.getInstanceAndAllWorkspacesUpgradeStatus;
|
||||
|
||||
if (loadingHealthStatus) {
|
||||
if (loadingHealthStatus || loadingUpgradeStatus) {
|
||||
return <SettingsSectionSkeletonLoader />;
|
||||
}
|
||||
|
||||
@@ -38,6 +51,15 @@ export const SettingsAdminHealthStatus = () => {
|
||||
loading={loadingHealthStatus}
|
||||
/>
|
||||
</Section>
|
||||
{upgradeStatus && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Upgrade Status`}
|
||||
description={t`Upgrade health across instance and workspaces`}
|
||||
/>
|
||||
<SettingsAdminUpgradeStatusListCard upgradeStatus={upgradeStatus} />
|
||||
</Section>
|
||||
)}
|
||||
<SettingsAdminMaintenanceMode />
|
||||
</>
|
||||
);
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { SettingsAdminUpgradeStatusRightContainer } from '@/settings/admin-panel/health-status/components/SettingsAdminUpgradeStatusRightContainer';
|
||||
import { SettingsListCard } from '@/settings/components/SettingsListCard';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconId,
|
||||
IconProgressCheck,
|
||||
IconStatusChange,
|
||||
type IconComponent,
|
||||
} from 'twenty-ui/display';
|
||||
import { type GetInstanceAndAllWorkspacesUpgradeStatusQuery } from '~/generated-admin/graphql';
|
||||
|
||||
type InstanceAndAllWorkspacesUpgradeStatus =
|
||||
GetInstanceAndAllWorkspacesUpgradeStatusQuery['getInstanceAndAllWorkspacesUpgradeStatus'];
|
||||
|
||||
export type UpgradeStatusRowKind =
|
||||
| 'inferred-version'
|
||||
| 'instance-status'
|
||||
| 'workspaces-status';
|
||||
|
||||
export type UpgradeStatusRow = {
|
||||
id: UpgradeStatusRowKind;
|
||||
kind: UpgradeStatusRowKind;
|
||||
label: string;
|
||||
inferredVersion: string | null | undefined;
|
||||
instanceHealth: InstanceAndAllWorkspacesUpgradeStatus['instanceUpgradeStatus']['health'];
|
||||
behindCount: number;
|
||||
failedCount: number;
|
||||
};
|
||||
|
||||
const ROW_ICON_BY_KIND: Record<UpgradeStatusRowKind, IconComponent> = {
|
||||
'inferred-version': IconId,
|
||||
'instance-status': IconProgressCheck,
|
||||
'workspaces-status': IconStatusChange,
|
||||
};
|
||||
|
||||
const SETTINGS_PATH_BY_KIND: Record<UpgradeStatusRowKind, SettingsPath> = {
|
||||
'inferred-version': SettingsPath.AdminPanelInferredVersion,
|
||||
'instance-status': SettingsPath.AdminPanelInstanceStatus,
|
||||
'workspaces-status': SettingsPath.AdminPanelWorkspacesStatus,
|
||||
};
|
||||
|
||||
type SettingsAdminUpgradeStatusListCardProps = {
|
||||
upgradeStatus: Pick<
|
||||
InstanceAndAllWorkspacesUpgradeStatus,
|
||||
'instanceUpgradeStatus' | 'workspacesBehind' | 'workspacesFailed'
|
||||
>;
|
||||
};
|
||||
|
||||
export const SettingsAdminUpgradeStatusListCard = ({
|
||||
upgradeStatus,
|
||||
}: SettingsAdminUpgradeStatusListCardProps) => {
|
||||
const sharedRowProps = {
|
||||
inferredVersion: upgradeStatus.instanceUpgradeStatus.inferredVersion,
|
||||
instanceHealth: upgradeStatus.instanceUpgradeStatus.health,
|
||||
behindCount: upgradeStatus.workspacesBehind.length,
|
||||
failedCount: upgradeStatus.workspacesFailed.length,
|
||||
};
|
||||
|
||||
const items: UpgradeStatusRow[] = [
|
||||
{
|
||||
id: 'inferred-version',
|
||||
kind: 'inferred-version',
|
||||
label: t`Inferred version`,
|
||||
...sharedRowProps,
|
||||
},
|
||||
{
|
||||
id: 'instance-status',
|
||||
kind: 'instance-status',
|
||||
label: t`Instance status`,
|
||||
...sharedRowProps,
|
||||
},
|
||||
{
|
||||
id: 'workspaces-status',
|
||||
kind: 'workspaces-status',
|
||||
label: t`Workspaces status`,
|
||||
...sharedRowProps,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsListCard<UpgradeStatusRow>
|
||||
items={items}
|
||||
rounded={true}
|
||||
RowIconFn={(item) => ROW_ICON_BY_KIND[item.kind]}
|
||||
getItemLabel={(item) => item.label}
|
||||
RowRightComponent={SettingsAdminUpgradeStatusRightContainer}
|
||||
to={(item) => getSettingsPath(SETTINGS_PATH_BY_KIND[item.kind])}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { type UpgradeStatusRow } from '@/settings/admin-panel/health-status/components/SettingsAdminUpgradeStatusListCard';
|
||||
import { getUpgradeHealthStatusBadge } from '@/settings/admin-panel/utils/getUpgradeHealthStatusBadge';
|
||||
import { getWorkspacesUpgradeHealth } from '@/settings/admin-panel/utils/getWorkspacesUpgradeHealth';
|
||||
import { getWorkspacesUpgradeHealthText } from '@/settings/admin-panel/utils/getWorkspacesUpgradeHealthText';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Status } from 'twenty-ui/display';
|
||||
|
||||
export const SettingsAdminUpgradeStatusRightContainer = ({
|
||||
item,
|
||||
}: {
|
||||
item: UpgradeStatusRow;
|
||||
}) => {
|
||||
if (item.kind === 'inferred-version') {
|
||||
return (
|
||||
<Status
|
||||
color="gray"
|
||||
text={item.inferredVersion ?? t`Unknown`}
|
||||
weight="medium"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.kind === 'instance-status') {
|
||||
const badge = getUpgradeHealthStatusBadge(item.instanceHealth);
|
||||
|
||||
return <Status color={badge.color} text={badge.label} weight="medium" />;
|
||||
}
|
||||
|
||||
const workspacesUpgradeHealth = getWorkspacesUpgradeHealth(
|
||||
item.behindCount,
|
||||
item.failedCount,
|
||||
);
|
||||
const workspacesUpgradeHealthBadge = getUpgradeHealthStatusBadge(
|
||||
workspacesUpgradeHealth,
|
||||
);
|
||||
const workspacesUpgradeHealthText = getWorkspacesUpgradeHealthText(
|
||||
item.behindCount,
|
||||
item.failedCount,
|
||||
workspacesUpgradeHealthBadge.label,
|
||||
);
|
||||
|
||||
return (
|
||||
<Status
|
||||
color={workspacesUpgradeHealthBadge.color}
|
||||
text={workspacesUpgradeHealthText}
|
||||
weight="medium"
|
||||
/>
|
||||
);
|
||||
};
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { IconChevronDown, IconChevronRight } from 'twenty-ui/display';
|
||||
import { AnimatedExpandableContainer, Card } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledAccordionHeaderButton = styled.button`
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
justify-content: space-between;
|
||||
padding: ${themeCssVariables.spacing[3]} ${themeCssVariables.spacing[4]};
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledAccordionHeaderButtonDisabled = styled(StyledAccordionHeaderButton)`
|
||||
cursor: default;
|
||||
`;
|
||||
|
||||
const StyledAccordionContent = styled.div`
|
||||
border-top: 1px solid ${themeCssVariables.border.color.medium};
|
||||
padding: ${themeCssVariables.spacing[3]} ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledWorkspaceList = styled.ul`
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
const StyledWorkspaceListItem = styled.li`
|
||||
border-bottom: 1px solid ${themeCssVariables.border.color.light};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledWorkspaceLink = styled(Link)`
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
display: block;
|
||||
padding: ${themeCssVariables.spacing[2]} 0;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
type WorkspaceUpgradeRefItem = {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
};
|
||||
|
||||
type SettingsAdminWorkspacesByHealthAccordionProps = {
|
||||
filledLabel: string;
|
||||
emptyLabel: string;
|
||||
workspaces: WorkspaceUpgradeRefItem[];
|
||||
defaultExpanded?: boolean;
|
||||
};
|
||||
|
||||
export const SettingsAdminWorkspacesByHealthAccordion = ({
|
||||
filledLabel,
|
||||
emptyLabel,
|
||||
workspaces,
|
||||
defaultExpanded = false,
|
||||
}: SettingsAdminWorkspacesByHealthAccordionProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||
const hasWorkspaces = workspaces.length > 0;
|
||||
|
||||
return (
|
||||
<Card rounded={true}>
|
||||
{hasWorkspaces ? (
|
||||
<StyledAccordionHeaderButton
|
||||
onClick={() => setIsExpanded((currentValue) => !currentValue)}
|
||||
>
|
||||
<span>{filledLabel}</span>
|
||||
{isExpanded ? (
|
||||
<IconChevronDown size={16} />
|
||||
) : (
|
||||
<IconChevronRight size={16} />
|
||||
)}
|
||||
</StyledAccordionHeaderButton>
|
||||
) : (
|
||||
<StyledAccordionHeaderButtonDisabled>
|
||||
<span>{emptyLabel}</span>
|
||||
</StyledAccordionHeaderButtonDisabled>
|
||||
)}
|
||||
{hasWorkspaces && (
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isExpanded}
|
||||
dimension="height"
|
||||
mode="scroll-height"
|
||||
>
|
||||
<StyledAccordionContent>
|
||||
<StyledWorkspaceList>
|
||||
{workspaces.map((workspace) => (
|
||||
<StyledWorkspaceListItem key={workspace.id}>
|
||||
<StyledWorkspaceLink
|
||||
to={getSettingsPath(
|
||||
SettingsPath.AdminPanelWorkspaceDetail,
|
||||
{ workspaceId: workspace.id },
|
||||
)}
|
||||
>
|
||||
{workspace.name ?? t`Unknown workspace`}
|
||||
{' - '}
|
||||
{workspace.id}
|
||||
</StyledWorkspaceLink>
|
||||
</StyledWorkspaceListItem>
|
||||
))}
|
||||
</StyledWorkspaceList>
|
||||
</StyledAccordionContent>
|
||||
</AnimatedExpandableContainer>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { getUpgradeHealthStatusBadge } from '@/settings/admin-panel/utils/getUpgradeHealthStatusBadge';
|
||||
import { getWorkspacesUpgradeHealth } from '@/settings/admin-panel/utils/getWorkspacesUpgradeHealth';
|
||||
import { getWorkspacesUpgradeHealthText } from '@/settings/admin-panel/utils/getWorkspacesUpgradeHealthText';
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useContext } from 'react';
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconClock,
|
||||
IconStatusChange,
|
||||
IconX,
|
||||
Status,
|
||||
} from 'twenty-ui/display';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
|
||||
|
||||
type SettingsAdminWorkspacesStatusSummaryCardProps = {
|
||||
behindCount: number;
|
||||
failedCount: number;
|
||||
computedAt: string | undefined;
|
||||
};
|
||||
|
||||
export const SettingsAdminWorkspacesStatusSummaryCard = ({
|
||||
behindCount,
|
||||
failedCount,
|
||||
computedAt,
|
||||
}: SettingsAdminWorkspacesStatusSummaryCardProps) => {
|
||||
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
|
||||
const { localeCatalog } = useAtomStateValue(dateLocaleState);
|
||||
|
||||
const workspacesUpgradeHealth = getWorkspacesUpgradeHealth(
|
||||
behindCount,
|
||||
failedCount,
|
||||
);
|
||||
const workspacesUpgradeHealthBadge = getUpgradeHealthStatusBadge(
|
||||
workspacesUpgradeHealth,
|
||||
);
|
||||
const workspacesUpgradeHealthText = getWorkspacesUpgradeHealthText(
|
||||
behindCount,
|
||||
failedCount,
|
||||
workspacesUpgradeHealthBadge.label,
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsTableCard
|
||||
items={[
|
||||
{
|
||||
Icon: IconStatusChange,
|
||||
label: t`Upgrade health`,
|
||||
value: (
|
||||
<Status
|
||||
color={workspacesUpgradeHealthBadge.color}
|
||||
text={workspacesUpgradeHealthText}
|
||||
weight="medium"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
Icon: IconAlertTriangle,
|
||||
label: t`Behind`,
|
||||
value: behindCount,
|
||||
},
|
||||
{
|
||||
Icon: IconX,
|
||||
label: t`Failed`,
|
||||
value: failedCount,
|
||||
},
|
||||
{
|
||||
Icon: IconClock,
|
||||
label: t`Computed at`,
|
||||
value:
|
||||
formatDateTimeString({
|
||||
value: computedAt,
|
||||
timeZone,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
localeCatalog,
|
||||
}) || t`N/A`,
|
||||
},
|
||||
]}
|
||||
gridAutoColumns="3fr 4fr"
|
||||
/>
|
||||
);
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const REFRESH_UPGRADE_STATUS = gql`
|
||||
mutation RefreshUpgradeStatus {
|
||||
refreshUpgradeStatus {
|
||||
instanceUpgradeStatus {
|
||||
inferredVersion
|
||||
health
|
||||
latestCommand {
|
||||
name
|
||||
status
|
||||
executedByVersion
|
||||
errorMessage
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
workspacesBehind {
|
||||
id
|
||||
name
|
||||
}
|
||||
workspacesFailed {
|
||||
id
|
||||
name
|
||||
}
|
||||
computedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_INSTANCE_AND_ALL_WORKSPACES_UPGRADE_STATUS = gql`
|
||||
query GetInstanceAndAllWorkspacesUpgradeStatus {
|
||||
getInstanceAndAllWorkspacesUpgradeStatus {
|
||||
instanceUpgradeStatus {
|
||||
inferredVersion
|
||||
health
|
||||
latestCommand {
|
||||
name
|
||||
status
|
||||
executedByVersion
|
||||
errorMessage
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
workspacesBehind {
|
||||
id
|
||||
name
|
||||
}
|
||||
workspacesFailed {
|
||||
id
|
||||
name
|
||||
}
|
||||
computedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type ThemeColor } from 'twenty-ui/theme';
|
||||
import { UpgradeHealth } from '~/generated-admin/graphql';
|
||||
|
||||
type UpgradeHealthStatusBadge = {
|
||||
color: ThemeColor;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const getUpgradeHealthStatusBadge = (
|
||||
health: UpgradeHealth | undefined,
|
||||
): UpgradeHealthStatusBadge => {
|
||||
switch (health) {
|
||||
case UpgradeHealth.UP_TO_DATE:
|
||||
return { color: 'green', label: t`Up to date` };
|
||||
case UpgradeHealth.BEHIND:
|
||||
return { color: 'orange', label: t`Behind` };
|
||||
case UpgradeHealth.FAILED:
|
||||
return { color: 'red', label: t`Failed` };
|
||||
default:
|
||||
return { color: 'gray', label: t`Unknown` };
|
||||
}
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { UpgradeHealth } from '~/generated-admin/graphql';
|
||||
|
||||
export const getWorkspacesUpgradeHealth = (
|
||||
behindCount: number,
|
||||
failedCount: number,
|
||||
): UpgradeHealth => {
|
||||
if (failedCount > 0) {
|
||||
return UpgradeHealth.FAILED;
|
||||
}
|
||||
|
||||
if (behindCount > 0) {
|
||||
return UpgradeHealth.BEHIND;
|
||||
}
|
||||
|
||||
return UpgradeHealth.UP_TO_DATE;
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { plural } from '@lingui/core/macro';
|
||||
|
||||
export const getWorkspacesUpgradeHealthText = (
|
||||
behindCount: number,
|
||||
failedCount: number,
|
||||
upToDateLabel: string,
|
||||
): string => {
|
||||
if (failedCount > 0 && behindCount > 0) {
|
||||
return plural(failedCount + behindCount, {
|
||||
one: '# workspace failed or behind',
|
||||
other: '# workspaces failed or behind',
|
||||
});
|
||||
}
|
||||
|
||||
if (failedCount > 0) {
|
||||
return plural(failedCount, {
|
||||
one: '# workspace failed',
|
||||
other: '# workspaces failed',
|
||||
});
|
||||
}
|
||||
|
||||
if (behindCount > 0) {
|
||||
return plural(behindCount, {
|
||||
one: '# workspace behind',
|
||||
other: '# workspaces behind',
|
||||
});
|
||||
}
|
||||
|
||||
return upToDateLabel;
|
||||
};
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
type WorkflowStep,
|
||||
type WorkflowTrigger,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
jest.mock('uuid');
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
jest.mock('uuid');
|
||||
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { H2Title, IconId } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
GetInstanceAndAllWorkspacesUpgradeStatusDocument,
|
||||
RefreshUpgradeStatusDocument,
|
||||
} from '~/generated-admin/graphql';
|
||||
|
||||
const StyledRefreshButtonContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: ${themeCssVariables.spacing[4]};
|
||||
margin-top: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
export const SettingsAdminInferredVersion = () => {
|
||||
const apolloAdminClient = useApolloAdminClient();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
|
||||
const {
|
||||
data,
|
||||
refetch,
|
||||
loading: isLoadingUpgradeStatus,
|
||||
} = useQuery(GetInstanceAndAllWorkspacesUpgradeStatusDocument, {
|
||||
client: apolloAdminClient,
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
const [refreshUpgradeStatus, { loading: isRefreshingUpgradeStatus }] =
|
||||
useMutation(RefreshUpgradeStatusDocument, {
|
||||
client: apolloAdminClient,
|
||||
});
|
||||
|
||||
const inferredVersion =
|
||||
data?.getInstanceAndAllWorkspacesUpgradeStatus.instanceUpgradeStatus
|
||||
.inferredVersion;
|
||||
|
||||
const handleRefreshUpgradeStatus = async () => {
|
||||
try {
|
||||
await refreshUpgradeStatus();
|
||||
await refetch();
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Upgrade status refreshed`,
|
||||
});
|
||||
} catch (error) {
|
||||
enqueueErrorSnackBar({
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t`Failed to refresh upgrade status`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
links={[
|
||||
{
|
||||
children: t`Other`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`Admin Panel - Health`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanelHealthStatus),
|
||||
},
|
||||
{
|
||||
children: t`Inferred version`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Inferred version`}
|
||||
description={t`Detected application version running on this instance`}
|
||||
/>
|
||||
<SettingsTableCard
|
||||
items={[
|
||||
{
|
||||
Icon: IconId,
|
||||
label: t`Inferred version`,
|
||||
value: inferredVersion ?? t`Unknown`,
|
||||
},
|
||||
]}
|
||||
gridAutoColumns="3fr 4fr"
|
||||
/>
|
||||
<StyledRefreshButtonContainer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
title={t`Refresh status`}
|
||||
onClick={handleRefreshUpgradeStatus}
|
||||
disabled={isRefreshingUpgradeStatus || isLoadingUpgradeStatus}
|
||||
/>
|
||||
</StyledRefreshButtonContainer>
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,182 @@
|
||||
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
|
||||
import { getUpgradeHealthStatusBadge } from '@/settings/admin-panel/utils/getUpgradeHealthStatusBadge';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useContext } from 'react';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { formatUpgradeCommandName, getSettingsPath } from 'twenty-shared/utils';
|
||||
import {
|
||||
H2Title,
|
||||
IconAlertTriangle,
|
||||
IconCalendar,
|
||||
IconProgressCheck,
|
||||
IconStatusChange,
|
||||
Status,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
GetInstanceAndAllWorkspacesUpgradeStatusDocument,
|
||||
RefreshUpgradeStatusDocument,
|
||||
} from '~/generated-admin/graphql';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
|
||||
|
||||
const StyledRefreshButtonContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: ${themeCssVariables.spacing[4]};
|
||||
margin-top: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
const StyledCommandValue = styled.span`
|
||||
word-break: break-word;
|
||||
`;
|
||||
|
||||
export const SettingsAdminInstanceStatus = () => {
|
||||
const apolloAdminClient = useApolloAdminClient();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
|
||||
const { localeCatalog } = useAtomStateValue(dateLocaleState);
|
||||
|
||||
const {
|
||||
data,
|
||||
refetch,
|
||||
loading: isLoadingUpgradeStatus,
|
||||
} = useQuery(GetInstanceAndAllWorkspacesUpgradeStatusDocument, {
|
||||
client: apolloAdminClient,
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
const [refreshUpgradeStatus, { loading: isRefreshingUpgradeStatus }] =
|
||||
useMutation(RefreshUpgradeStatusDocument, {
|
||||
client: apolloAdminClient,
|
||||
});
|
||||
|
||||
const instanceUpgradeStatus =
|
||||
data?.getInstanceAndAllWorkspacesUpgradeStatus.instanceUpgradeStatus;
|
||||
const instanceHealth = instanceUpgradeStatus?.health;
|
||||
const instanceLatestCommand = instanceUpgradeStatus?.latestCommand;
|
||||
|
||||
const instanceHealthBadge = getUpgradeHealthStatusBadge(instanceHealth);
|
||||
|
||||
const formattedInstanceLastUpdated = formatDateTimeString({
|
||||
value: instanceLatestCommand?.createdAt,
|
||||
timeZone,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
localeCatalog,
|
||||
});
|
||||
|
||||
const handleRefreshUpgradeStatus = async () => {
|
||||
try {
|
||||
await refreshUpgradeStatus();
|
||||
await refetch();
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Upgrade status refreshed`,
|
||||
});
|
||||
} catch (error) {
|
||||
enqueueErrorSnackBar({
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t`Failed to refresh upgrade status`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
links={[
|
||||
{
|
||||
children: t`Other`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`Admin Panel - Health`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanelHealthStatus),
|
||||
},
|
||||
{
|
||||
children: t`Instance status`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Instance status`}
|
||||
description={t`Health of the latest instance command`}
|
||||
/>
|
||||
<SettingsTableCard
|
||||
items={[
|
||||
{
|
||||
Icon: IconProgressCheck,
|
||||
label: t`Status`,
|
||||
value: (
|
||||
<Status
|
||||
color={instanceHealthBadge.color}
|
||||
text={instanceHealthBadge.label}
|
||||
weight="medium"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
Icon: IconCalendar,
|
||||
label: t`Last command`,
|
||||
value: (
|
||||
<StyledCommandValue>
|
||||
{instanceLatestCommand?.name
|
||||
? formatUpgradeCommandName(instanceLatestCommand.name)
|
||||
: t`None`}
|
||||
</StyledCommandValue>
|
||||
),
|
||||
},
|
||||
{
|
||||
Icon: IconStatusChange,
|
||||
label: t`Last command result`,
|
||||
value: instanceLatestCommand?.status
|
||||
? instanceLatestCommand.status === 'completed'
|
||||
? t`Completed`
|
||||
: t`Failed`
|
||||
: t`N/A`,
|
||||
},
|
||||
{
|
||||
Icon: IconCalendar,
|
||||
label: t`Last updated`,
|
||||
value: isNonEmptyString(formattedInstanceLastUpdated)
|
||||
? formattedInstanceLastUpdated
|
||||
: t`N/A`,
|
||||
},
|
||||
...(instanceLatestCommand?.errorMessage
|
||||
? [
|
||||
{
|
||||
Icon: IconAlertTriangle,
|
||||
label: t`Last error`,
|
||||
value: instanceLatestCommand.errorMessage,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
gridAutoColumns="3fr 4fr"
|
||||
/>
|
||||
<StyledRefreshButtonContainer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
title={t`Refresh status`}
|
||||
onClick={handleRefreshUpgradeStatus}
|
||||
disabled={isRefreshingUpgradeStatus || isLoadingUpgradeStatus}
|
||||
/>
|
||||
</StyledRefreshButtonContainer>
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,11 @@ import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getImageAbsoluteURI, getSettingsPath } from 'twenty-shared/utils';
|
||||
import {
|
||||
getImageAbsoluteURI,
|
||||
getSettingsPath,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
|
||||
@@ -15,9 +19,9 @@ import { useHandleImpersonate } from '@/settings/admin-panel/hooks/useHandleImpe
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
@@ -64,9 +68,10 @@ export const SettingsAdminUserDetail = () => {
|
||||
const { handleImpersonate, impersonatingUserId } = useHandleImpersonate();
|
||||
|
||||
const effectiveTabId = activeTabId || userLookupResult?.workspaces?.[0]?.id;
|
||||
const user = userLookupResult?.user;
|
||||
|
||||
const userFullName = `${userLookupResult?.user.firstName || ''} ${
|
||||
userLookupResult?.user.lastName || ''
|
||||
const userFullName = `${user?.firstName || ''} ${
|
||||
user?.lastName || ''
|
||||
}`.trim();
|
||||
|
||||
const activeWorkspace = userLookupResult?.workspaces.find(
|
||||
@@ -95,18 +100,18 @@ export const SettingsAdminUserDetail = () => {
|
||||
{
|
||||
Icon: IconMail,
|
||||
label: t`Email`,
|
||||
value: userLookupResult?.user.email,
|
||||
value: user?.email,
|
||||
},
|
||||
{
|
||||
Icon: IconId,
|
||||
label: t`ID`,
|
||||
value: userLookupResult?.user.id,
|
||||
value: user?.id,
|
||||
},
|
||||
{
|
||||
Icon: IconCalendar,
|
||||
label: t`Created`,
|
||||
value: userLookupResult?.user.createdAt
|
||||
? new Date(userLookupResult.user.createdAt).toLocaleDateString()
|
||||
value: user?.createdAt
|
||||
? new Date(user.createdAt).toLocaleDateString()
|
||||
: '',
|
||||
},
|
||||
];
|
||||
@@ -159,30 +164,29 @@ export const SettingsAdminUserDetail = () => {
|
||||
<SettingsAdminWorkspaceContent
|
||||
activeWorkspace={activeWorkspace}
|
||||
/>
|
||||
{currentUser?.canImpersonate && activeWorkspace && (
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
Icon={IconEyeShare}
|
||||
variant="primary"
|
||||
accent="default"
|
||||
title={
|
||||
activeWorkspace.allowImpersonation === false
|
||||
? t`Impersonation is disabled for this workspace`
|
||||
: t`Impersonate`
|
||||
}
|
||||
onClick={() =>
|
||||
handleImpersonate(
|
||||
userLookupResult.user.id,
|
||||
activeWorkspace.id,
|
||||
)
|
||||
}
|
||||
disabled={
|
||||
impersonatingUserId !== null ||
|
||||
activeWorkspace.allowImpersonation === false
|
||||
}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
)}
|
||||
{currentUser?.canImpersonate &&
|
||||
activeWorkspace &&
|
||||
isDefined(user) && (
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
Icon={IconEyeShare}
|
||||
variant="primary"
|
||||
accent="default"
|
||||
title={
|
||||
activeWorkspace.allowImpersonation === false
|
||||
? t`Impersonation is disabled for this workspace`
|
||||
: t`Impersonate`
|
||||
}
|
||||
onClick={() =>
|
||||
handleImpersonate(user.id, activeWorkspace.id)
|
||||
}
|
||||
disabled={
|
||||
impersonatingUserId !== null ||
|
||||
activeWorkspace.allowImpersonation === false
|
||||
}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
)}
|
||||
</Section>
|
||||
</>
|
||||
)}
|
||||
|
||||
+20
-5
@@ -19,16 +19,16 @@ import { WORKSPACE_LOOKUP_ADMIN_PANEL } from '@/settings/admin-panel/graphql/que
|
||||
import { useFeatureFlagState } from '@/settings/admin-panel/hooks/useFeatureFlagState';
|
||||
import { useHandleImpersonate } from '@/settings/admin-panel/hooks/useHandleImpersonate';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
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 { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import {
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
type FeatureFlagKey,
|
||||
type GetAdminWorkspaceChatThreadsQuery,
|
||||
type WorkspaceLookupAdminPanelQuery,
|
||||
GetUpgradeStatusDocument,
|
||||
UpdateWorkspaceFeatureFlagDocument,
|
||||
} from '~/generated-admin/graphql';
|
||||
|
||||
@@ -106,6 +107,15 @@ export const SettingsAdminWorkspaceDetail = () => {
|
||||
effectiveTabId !== WORKSPACE_DETAIL_TAB_IDS.CHATS,
|
||||
},
|
||||
);
|
||||
const { data: workspaceUpgradeStatusData } = useQuery(
|
||||
GetUpgradeStatusDocument,
|
||||
{
|
||||
client: apolloAdminClient,
|
||||
variables: { workspaceIds: workspaceId ? [workspaceId] : [] },
|
||||
skip: !workspaceId,
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
);
|
||||
|
||||
const threads = threadsData?.getAdminWorkspaceChatThreads ?? [];
|
||||
|
||||
@@ -211,7 +221,12 @@ export const SettingsAdminWorkspaceDetail = () => {
|
||||
/>
|
||||
|
||||
{effectiveTabId === WORKSPACE_DETAIL_TAB_IDS.INFO && workspace && (
|
||||
<SettingsAdminWorkspaceContent activeWorkspace={workspace} />
|
||||
<SettingsAdminWorkspaceContent
|
||||
activeWorkspace={workspace}
|
||||
workspaceUpgradeStatus={workspaceUpgradeStatusData?.getUpgradeStatus?.find(
|
||||
(status) => status?.workspaceId === workspaceId,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{effectiveTabId === WORKSPACE_DETAIL_TAB_IDS.BILLING &&
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
|
||||
import { SettingsAdminWorkspacesByHealthAccordion } from '@/settings/admin-panel/health-status/components/SettingsAdminWorkspacesByHealthAccordion';
|
||||
import { SettingsAdminWorkspacesStatusSummaryCard } from '@/settings/admin-panel/health-status/components/SettingsAdminWorkspacesStatusSummaryCard';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { plural, t } from '@lingui/core/macro';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
GetInstanceAndAllWorkspacesUpgradeStatusDocument,
|
||||
RefreshUpgradeStatusDocument,
|
||||
} from '~/generated-admin/graphql';
|
||||
|
||||
const StyledRefreshButtonContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: ${themeCssVariables.spacing[4]};
|
||||
margin-top: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
const StyledAccordionCardsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
export const SettingsAdminWorkspacesStatus = () => {
|
||||
const apolloAdminClient = useApolloAdminClient();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
|
||||
const {
|
||||
data,
|
||||
refetch,
|
||||
loading: isLoadingUpgradeStatus,
|
||||
} = useQuery(GetInstanceAndAllWorkspacesUpgradeStatusDocument, {
|
||||
client: apolloAdminClient,
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
const [refreshUpgradeStatus, { loading: isRefreshingUpgradeStatus }] =
|
||||
useMutation(RefreshUpgradeStatusDocument, {
|
||||
client: apolloAdminClient,
|
||||
});
|
||||
|
||||
const upgradeStatus = data?.getInstanceAndAllWorkspacesUpgradeStatus;
|
||||
const behindCount = upgradeStatus?.workspacesBehind.length ?? 0;
|
||||
const failedCount = upgradeStatus?.workspacesFailed.length ?? 0;
|
||||
|
||||
const handleRefreshUpgradeStatus = async () => {
|
||||
try {
|
||||
await refreshUpgradeStatus();
|
||||
await refetch();
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Upgrade status refreshed`,
|
||||
});
|
||||
} catch (error) {
|
||||
enqueueErrorSnackBar({
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t`Failed to refresh upgrade status`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
links={[
|
||||
{
|
||||
children: t`Other`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`Admin Panel - Health`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanelHealthStatus),
|
||||
},
|
||||
{
|
||||
children: t`Workspaces status`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Workspaces status`}
|
||||
description={t`Upgrade health across all workspaces`}
|
||||
/>
|
||||
<SettingsAdminWorkspacesStatusSummaryCard
|
||||
behindCount={behindCount}
|
||||
failedCount={failedCount}
|
||||
computedAt={upgradeStatus?.computedAt}
|
||||
/>
|
||||
<StyledRefreshButtonContainer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
title={t`Refresh status`}
|
||||
onClick={handleRefreshUpgradeStatus}
|
||||
disabled={isRefreshingUpgradeStatus || isLoadingUpgradeStatus}
|
||||
/>
|
||||
</StyledRefreshButtonContainer>
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Detail per workspace`}
|
||||
description={t`Workspace lists by upgrade status`}
|
||||
/>
|
||||
<StyledAccordionCardsContainer>
|
||||
<SettingsAdminWorkspacesByHealthAccordion
|
||||
filledLabel={plural(behindCount, {
|
||||
one: '# workspace behind',
|
||||
other: '# workspaces behind',
|
||||
})}
|
||||
emptyLabel={t`No workspace behind`}
|
||||
workspaces={upgradeStatus?.workspacesBehind ?? []}
|
||||
defaultExpanded={true}
|
||||
/>
|
||||
<SettingsAdminWorkspacesByHealthAccordion
|
||||
filledLabel={plural(failedCount, {
|
||||
one: '# workspace failed',
|
||||
other: '# workspaces failed',
|
||||
})}
|
||||
emptyLabel={t`No workspace failed`}
|
||||
workspaces={upgradeStatus?.workspacesFailed ?? []}
|
||||
/>
|
||||
</StyledAccordionCardsContainer>
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.3.0', 1777308014234)
|
||||
export class AddUpgradeMigrationWorkspaceIdIndexFastInstanceCommand
|
||||
implements FastInstanceCommand
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_UPGRADE_MIGRATION_WORKSPACE_ID_NAME_ATTEMPT" ON "core"."upgradeMigration" ("workspaceId", "name", "attempt") WHERE "workspaceId" IS NOT NULL',
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'DROP INDEX IF EXISTS "core"."IDX_UPGRADE_MIGRATION_WORKSPACE_ID_NAME_ATTEMPT"',
|
||||
);
|
||||
}
|
||||
}
|
||||
+6
-4
@@ -3,21 +3,22 @@
|
||||
import { AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-instance-command-fast-1775129420309-add-view-field-group-id-index-on-view-field';
|
||||
import { MigrateMessagingCalendarToCoreFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-instance-command-fast-1775165049548-migrate-messaging-calendar-to-core';
|
||||
import { AddEmailThreadWidgetTypeFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-instance-command-fast-1775200000000-add-email-thread-widget-type';
|
||||
import { AddStandalonePageFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-instance-command-fast-1775752781995-add-standalone-page';
|
||||
import { AddPermissionFlagRoleIdIndexFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-22/1-22-instance-command-fast-1775749486425-add-permission-flag-role-id-index';
|
||||
import { AddTableWidgetViewTypeFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-instance-command-fast-1775752190522-add-table-widget-view-type';
|
||||
import { AddWorkspaceIdToIndirectEntitiesFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-22/1-22-instance-command-fast-1775758621017-add-workspace-id-to-indirect-entities';
|
||||
import { AddWorkspaceIdIndexesAndFksFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-22/1-22-instance-command-fast-1775761294897-add-workspace-id-indexes-and-fks-to-indirect-entities';
|
||||
import { DropObjectMetadataDataSourceFkFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-22/1-22-instance-command-fast-1775804361516-drop-object-metadata-data-source-fk';
|
||||
import { AddCreditBalanceToBillingCustomerFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-22/1-22-instance-command-fast-1776078919203-add-credit-balance-to-billing-customer';
|
||||
import { BackfillWorkspaceIdOnIndirectEntitiesSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/1-22/1-22-instance-command-slow-1775758621018-backfill-workspace-id-on-indirect-entities';
|
||||
import { DropWorkspaceVersionColumnFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-instance-command-fast-1785000000000-drop-workspace-version-column';
|
||||
import { AddConditionalAvailabilityExpressionToPageLayoutWidgetFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-instance-command-fast-1775654781000-add-conditional-availability-expression-to-page-layout-widget';
|
||||
import { AddTableWidgetViewTypeFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-instance-command-fast-1775752190522-add-table-widget-view-type';
|
||||
import { AddStandalonePageFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-instance-command-fast-1775752781995-add-standalone-page';
|
||||
import { AddGlobalObjectContextToCommandMenuItemAvailabilityTypeFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-instance-command-fast-1776090711153-add-global-object-context-to-command-menu-item-availability-type';
|
||||
import { AddPageLayoutIdToCommandMenuItemFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-instance-command-fast-1776168404836-add-page-layout-id-to-command-menu-item';
|
||||
import { AddConditionalAvailabilityExpressionToPageLayoutWidgetFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-instance-command-fast-1775654781000-add-conditional-availability-expression-to-page-layout-widget';
|
||||
import { DropWorkspaceVersionColumnFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-instance-command-fast-1785000000000-drop-workspace-version-column';
|
||||
import { AddIsPreInstalledToApplicationRegistrationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-1/2-1-instance-command-fast-1776886452831-add-is-pre-installed-to-application-registration';
|
||||
import { AddProviderExecutedToAgentMessagePartFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-1/2-1-instance-command-fast-1777012800000-add-provider-executed-to-agent-message-part';
|
||||
import { BackfillPageLayoutWidgetPositionSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-1/2-1-instance-command-slow-1795000002000-backfill-page-layout-widget-position';
|
||||
import { AddUpgradeMigrationWorkspaceIdIndexFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777308014234-add-upgrade-migration-workspace-id-index';
|
||||
import { AddCacheTokensToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777455269302-add-cache-tokens-to-agent-chat-thread';
|
||||
import { AddLogoToApplicationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777539664664-add-logo-to-application';
|
||||
import { AddDeletedAtToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777682000000-add-deleted-at-to-agent-chat-thread';
|
||||
@@ -38,6 +39,7 @@ export const INSTANCE_COMMANDS = [
|
||||
AddGlobalObjectContextToCommandMenuItemAvailabilityTypeFastInstanceCommand,
|
||||
AddPageLayoutIdToCommandMenuItemFastInstanceCommand,
|
||||
AddConditionalAvailabilityExpressionToPageLayoutWidgetFastInstanceCommand,
|
||||
AddUpgradeMigrationWorkspaceIdIndexFastInstanceCommand,
|
||||
AddIsPreInstalledToApplicationRegistrationFastInstanceCommand,
|
||||
AddProviderExecutedToAgentMessagePartFastInstanceCommand,
|
||||
BackfillPageLayoutWidgetPositionSlowInstanceCommand,
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@ export type CoreEntityCacheDataMap = {
|
||||
export type CoreEntityCacheKeyName = keyof CoreEntityCacheDataMap;
|
||||
|
||||
export const CORE_ENTITY_CACHE_KEYS: Record<CoreEntityCacheKeyName, string> = {
|
||||
workspaceEntity: 'core-entity:workspace',
|
||||
user: 'core-entity:user',
|
||||
userWorkspaceEntity: 'core-entity:user-workspace',
|
||||
workspaceEntity: 'workspace',
|
||||
user: 'user',
|
||||
userWorkspaceEntity: 'user-workspace',
|
||||
};
|
||||
|
||||
@@ -5,13 +5,18 @@ 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 { AppHealthIndicator } from 'src/engine/core-modules/admin-panel/indicators/app.health';
|
||||
import { ConnectedAccountHealth } from 'src/engine/core-modules/admin-panel/indicators/connected-account.health';
|
||||
import { DatabaseHealthIndicator } from 'src/engine/core-modules/admin-panel/indicators/database.health';
|
||||
import { RedisHealthIndicator } from 'src/engine/core-modules/admin-panel/indicators/redis.health';
|
||||
import { WorkerHealthIndicator } from 'src/engine/core-modules/admin-panel/indicators/worker.health';
|
||||
import { MaintenanceModeService } from 'src/engine/core-modules/admin-panel/maintenance-mode.service';
|
||||
import { AdminPanelBillingService } from 'src/engine/core-modules/admin-panel/services/admin-panel-billing.service';
|
||||
import { AdminPanelChatService } from 'src/engine/core-modules/admin-panel/services/admin-panel-chat.service';
|
||||
import { AdminPanelConfigService } from 'src/engine/core-modules/admin-panel/services/admin-panel-config.service';
|
||||
import { AdminPanelStatisticsService } from 'src/engine/core-modules/admin-panel/services/admin-panel-statistics.service';
|
||||
import { AdminPanelUserLookupService } from 'src/engine/core-modules/admin-panel/services/admin-panel-user-lookup.service';
|
||||
import { AdminPanelVersionService } from 'src/engine/core-modules/admin-panel/services/admin-panel-version.service';
|
||||
import { MaintenanceModeService } from 'src/engine/core-modules/admin-panel/maintenance-mode.service';
|
||||
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
|
||||
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
@@ -19,28 +24,24 @@ import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||
import { BillingCustomerEntity } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||
import { BillingPriceEntity } from 'src/engine/core-modules/billing/entities/billing-price.entity';
|
||||
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { AppHealthIndicator } from 'src/engine/core-modules/admin-panel/indicators/app.health';
|
||||
import { ConnectedAccountHealth } from 'src/engine/core-modules/admin-panel/indicators/connected-account.health';
|
||||
import { DatabaseHealthIndicator } from 'src/engine/core-modules/admin-panel/indicators/database.health';
|
||||
import { RedisHealthIndicator } from 'src/engine/core-modules/admin-panel/indicators/redis.health';
|
||||
import { WorkerHealthIndicator } from 'src/engine/core-modules/admin-panel/indicators/worker.health';
|
||||
import { ImpersonationModule } from 'src/engine/core-modules/impersonation/impersonation.module';
|
||||
import { KeyValuePairModule } from 'src/engine/core-modules/key-value-pair/key-value-pair.module';
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
import { RedisClientModule } from 'src/engine/core-modules/redis-client/redis-client.module';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module';
|
||||
import { UpgradeModule } from 'src/engine/core-modules/upgrade/upgrade.module';
|
||||
import { UsageModule } from 'src/engine/core-modules/usage/usage.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
|
||||
import { AgentMessageEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity';
|
||||
import { KeyValuePairModule } from 'src/engine/core-modules/key-value-pair/key-value-pair.module';
|
||||
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
@Module({
|
||||
@@ -72,6 +73,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
UsageModule,
|
||||
KeyValuePairModule,
|
||||
UserVarsModule,
|
||||
UpgradeModule,
|
||||
UserModule,
|
||||
],
|
||||
providers: [
|
||||
|
||||
+54
-26
@@ -3,32 +3,23 @@ import { Args, Int, Mutation, Query } from '@nestjs/graphql';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
import { In, type Repository } from 'typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, type Repository } from 'typeorm';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { InstanceAndAllWorkspacesUpgradeStatusDTO } from 'src/engine/core-modules/upgrade/dtos/instance-and-all-workspaces-upgrade-status.dto';
|
||||
import { WorkspaceUpgradeStatusDTO } from 'src/engine/core-modules/upgrade/dtos/workspace-upgrade-status.dto';
|
||||
import { UpgradeStatusService } from 'src/engine/core-modules/upgrade/services/upgrade-status.service';
|
||||
|
||||
import { AdminResolver } from 'src/engine/api/graphql/graphql-config/decorators/admin-resolver.decorator';
|
||||
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 { AdminPanelBillingService } from 'src/engine/core-modules/admin-panel/services/admin-panel-billing.service';
|
||||
import { AdminPanelChatService } from 'src/engine/core-modules/admin-panel/services/admin-panel-chat.service';
|
||||
import { AdminPanelConfigService } from 'src/engine/core-modules/admin-panel/services/admin-panel-config.service';
|
||||
import { AdminPanelStatisticsService } from 'src/engine/core-modules/admin-panel/services/admin-panel-statistics.service';
|
||||
import { AdminPanelUserLookupService } from 'src/engine/core-modules/admin-panel/services/admin-panel-user-lookup.service';
|
||||
import { AdminPanelVersionService } from 'src/engine/core-modules/admin-panel/services/admin-panel-version.service';
|
||||
import { MaintenanceModeService } from 'src/engine/core-modules/admin-panel/maintenance-mode.service';
|
||||
import { AdminChatThreadMessagesDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-chat-thread-messages.dto';
|
||||
import { AdminPanelRecentUserDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-recent-user.dto';
|
||||
import { AdminPanelTopWorkspaceDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-top-workspace.dto';
|
||||
import { AdminPanelWorkspaceBillingDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-billing.dto';
|
||||
import { AdminWorkspaceChatThreadDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-workspace-chat-thread.dto';
|
||||
import { AdminChatThreadMessagesDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-chat-thread-messages.dto';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
|
||||
import { AdminAiModelsDTO } from 'src/engine/core-modules/client-config/client-config.entity';
|
||||
import { UsageBreakdownItemDTO } from 'src/engine/core-modules/usage/dtos/usage-breakdown-item.dto';
|
||||
import { UsageAnalyticsService } from 'src/engine/core-modules/usage/services/usage-analytics.service';
|
||||
import { AiModelRole } from 'src/engine/metadata-modules/ai/ai-models/types/ai-model-role.enum';
|
||||
import { ConfigVariableDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
|
||||
import { ConfigVariablesDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variables.dto';
|
||||
import { DeleteJobsResponseDTO } from 'src/engine/core-modules/admin-panel/dtos/delete-jobs-response.dto';
|
||||
@@ -39,33 +30,45 @@ import { UpdateWorkspaceFeatureFlagInput } from 'src/engine/core-modules/admin-p
|
||||
import { UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.dto';
|
||||
import { UserLookupInput } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.input';
|
||||
import { VersionInfoDTO } from 'src/engine/core-modules/admin-panel/dtos/version-info.dto';
|
||||
import { HealthIndicatorId } from 'src/engine/core-modules/admin-panel/enums/health-indicator-id.enum';
|
||||
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 { MaintenanceModeService } from 'src/engine/core-modules/admin-panel/maintenance-mode.service';
|
||||
import { AdminPanelBillingService } from 'src/engine/core-modules/admin-panel/services/admin-panel-billing.service';
|
||||
import { AdminPanelChatService } from 'src/engine/core-modules/admin-panel/services/admin-panel-chat.service';
|
||||
import { AdminPanelConfigService } from 'src/engine/core-modules/admin-panel/services/admin-panel-config.service';
|
||||
import { AdminPanelStatisticsService } from 'src/engine/core-modules/admin-panel/services/admin-panel-statistics.service';
|
||||
import { AdminPanelUserLookupService } from 'src/engine/core-modules/admin-panel/services/admin-panel-user-lookup.service';
|
||||
import { AdminPanelVersionService } from 'src/engine/core-modules/admin-panel/services/admin-panel-version.service';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
|
||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||
import { AdminAiModelsDTO } from 'src/engine/core-modules/client-config/client-config.entity';
|
||||
import { FeatureFlagException } from 'src/engine/core-modules/feature-flag/feature-flag.exception';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { HealthIndicatorId } from 'src/engine/core-modules/admin-panel/enums/health-indicator-id.enum';
|
||||
import { type MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { type ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
||||
import { ConfigVariableGraphqlApiExceptionFilter } from 'src/engine/core-modules/twenty-config/filters/config-variable-graphql-api-exception.filter';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
|
||||
import { ModelsDevCatalogService } from 'src/engine/metadata-modules/ai/ai-models/services/models-dev-catalog.service';
|
||||
import { MODEL_FAMILY_LABELS } from 'src/engine/metadata-modules/ai/ai-models/constants/model-family-labels.const';
|
||||
import { type AiProviderConfig } from 'src/engine/metadata-modules/ai/ai-models/types/ai-provider-config.type';
|
||||
import { type AiProviderModelConfig } from 'src/engine/metadata-modules/ai/ai-models/types/ai-provider-model-config.type';
|
||||
import { extractConfigVariableName } from 'src/engine/metadata-modules/ai/ai-models/utils/extract-config-variable-name.util';
|
||||
import { DefaultAiCatalogService } from 'src/engine/metadata-modules/ai/ai-models/services/default-ai-catalog.service';
|
||||
import { UsageBreakdownItemDTO } from 'src/engine/core-modules/usage/dtos/usage-breakdown-item.dto';
|
||||
import { UsageAnalyticsService } from 'src/engine/core-modules/usage/services/usage-analytics.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AdminResolver } from 'src/engine/api/graphql/graphql-config/decorators/admin-resolver.decorator';
|
||||
import { AdminPanelGuard } from 'src/engine/guards/admin-panel-guard';
|
||||
import { ServerLevelImpersonateGuard } from 'src/engine/guards/server-level-impersonate.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { MODEL_FAMILY_LABELS } from 'src/engine/metadata-modules/ai/ai-models/constants/model-family-labels.const';
|
||||
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
|
||||
import { DefaultAiCatalogService } from 'src/engine/metadata-modules/ai/ai-models/services/default-ai-catalog.service';
|
||||
import { ModelsDevCatalogService } from 'src/engine/metadata-modules/ai/ai-models/services/models-dev-catalog.service';
|
||||
import { AiModelRole } from 'src/engine/metadata-modules/ai/ai-models/types/ai-model-role.enum';
|
||||
import { type AiProviderConfig } from 'src/engine/metadata-modules/ai/ai-models/types/ai-provider-config.type';
|
||||
import { type AiProviderModelConfig } from 'src/engine/metadata-modules/ai/ai-models/types/ai-provider-model-config.type';
|
||||
import { extractConfigVariableName } from 'src/engine/metadata-modules/ai/ai-models/utils/extract-config-variable-name.util';
|
||||
|
||||
import { AdminPanelHealthServiceDataDTO } from './dtos/admin-panel-health-service-data.dto';
|
||||
import { MaintenanceModeDTO } from './dtos/maintenance-mode.dto';
|
||||
@@ -73,7 +76,6 @@ import { ModelsDevModelSuggestionDTO } from './dtos/models-dev-model-suggestion.
|
||||
import { ModelsDevProviderSuggestionDTO } from './dtos/models-dev-provider-suggestion.dto';
|
||||
import { QueueMetricsDataDTO } from './dtos/queue-metrics-data.dto';
|
||||
import { SetMaintenanceModeInput } from './dtos/set-maintenance-mode.input';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
@AdminResolver()
|
||||
@@ -105,6 +107,7 @@ export class AdminPanelResolver {
|
||||
private readonly modelsDevCatalogService: ModelsDevCatalogService,
|
||||
private readonly usageAnalyticsService: UsageAnalyticsService,
|
||||
private readonly maintenanceModeService: MaintenanceModeService,
|
||||
private readonly upgradeStatusService: UpgradeStatusService,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
) {}
|
||||
@@ -701,4 +704,29 @@ export class AdminPanelResolver {
|
||||
): Promise<ApplicationRegistrationEntity> {
|
||||
return this.applicationRegistrationService.findOneByIdGlobal(id);
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Query(() => InstanceAndAllWorkspacesUpgradeStatusDTO)
|
||||
async getInstanceAndAllWorkspacesUpgradeStatus(): Promise<InstanceAndAllWorkspacesUpgradeStatusDTO> {
|
||||
return this.upgradeStatusService.getInstanceAndAllWorkspacesStatus();
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Mutation(() => InstanceAndAllWorkspacesUpgradeStatusDTO)
|
||||
async refreshUpgradeStatus(): Promise<InstanceAndAllWorkspacesUpgradeStatusDTO> {
|
||||
return this.upgradeStatusService.refreshInstanceAndAllWorkspacesStatus();
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Query(() => [WorkspaceUpgradeStatusDTO])
|
||||
async getUpgradeStatus(
|
||||
@Args('workspaceIds', { type: () => [UUIDScalarType] })
|
||||
workspaceIds: string[],
|
||||
): Promise<WorkspaceUpgradeStatusDTO[]> {
|
||||
if (workspaceIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.upgradeStatusService.getWorkspaceStatuses(workspaceIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ class WorkspaceInfoDTO {
|
||||
|
||||
@ObjectType('UserLookup')
|
||||
export class UserLookup {
|
||||
@Field(() => UserInfoDTO)
|
||||
user: UserInfoDTO;
|
||||
@Field(() => UserInfoDTO, { nullable: true })
|
||||
user?: UserInfoDTO | null;
|
||||
|
||||
@Field(() => [WorkspaceInfoDTO])
|
||||
workspaces: WorkspaceInfoDTO[];
|
||||
|
||||
+11
-11
@@ -14,9 +14,9 @@ import {
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@@ -208,16 +208,16 @@ export class AdminPanelUserLookupService {
|
||||
const firstUser = workspaceUsers.find((wu) => isDefined(wu.user))?.user;
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: firstUser?.id ?? '',
|
||||
email: firstUser?.email ?? '',
|
||||
firstName: firstUser?.firstName,
|
||||
lastName: firstUser?.lastName,
|
||||
avatarUrl: firstUser
|
||||
? (avatarUrlsByUserId.get(firstUser.id) ?? null)
|
||||
: null,
|
||||
createdAt: firstUser?.createdAt ?? new Date(),
|
||||
},
|
||||
user: isDefined(firstUser)
|
||||
? {
|
||||
id: firstUser.id,
|
||||
email: firstUser.email,
|
||||
firstName: firstUser.firstName,
|
||||
lastName: firstUser.lastName,
|
||||
avatarUrl: avatarUrlsByUserId.get(firstUser.id) ?? null,
|
||||
createdAt: firstUser.createdAt,
|
||||
}
|
||||
: null,
|
||||
workspaces: [workspaceInfo],
|
||||
};
|
||||
}
|
||||
|
||||
+68
-46
@@ -4,11 +4,12 @@ import chalk from 'chalk';
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UpgradeHealthEnum } from 'twenty-shared/types';
|
||||
import { formatUpgradeCommandName } from 'twenty-shared/utils';
|
||||
import {
|
||||
type MigrationCursorStatus,
|
||||
UpgradeHealth,
|
||||
type InstanceUpgradeStatus,
|
||||
UpgradeStatusService,
|
||||
type WorkspaceStatus,
|
||||
type WorkspaceUpgradeStatus,
|
||||
} from 'src/engine/core-modules/upgrade/services/upgrade-status.service';
|
||||
|
||||
type UpgradeStatusOptions = {
|
||||
@@ -16,16 +17,16 @@ type UpgradeStatusOptions = {
|
||||
failedOnly?: boolean;
|
||||
};
|
||||
|
||||
type GroupedWorkspaceStatuses = {
|
||||
upToDate: WorkspaceStatus[];
|
||||
behind: WorkspaceStatus[];
|
||||
failed: WorkspaceStatus[];
|
||||
type GroupedWorkspaceUpgradeStatuses = {
|
||||
upToDate: WorkspaceUpgradeStatus[];
|
||||
behind: WorkspaceUpgradeStatus[];
|
||||
failed: WorkspaceUpgradeStatus[];
|
||||
};
|
||||
|
||||
const HEALTH_LABELS: Record<UpgradeHealth, string> = {
|
||||
'up-to-date': chalk.green('Up to date'),
|
||||
behind: chalk.yellow('Behind'),
|
||||
failed: chalk.red('Failed'),
|
||||
const HEALTH_LABELS: Record<UpgradeHealthEnum, string> = {
|
||||
[UpgradeHealthEnum.UP_TO_DATE]: chalk.green('Up to date'),
|
||||
[UpgradeHealthEnum.BEHIND]: chalk.yellow('Behind'),
|
||||
[UpgradeHealthEnum.FAILED]: chalk.red('Failed'),
|
||||
};
|
||||
|
||||
@Command({
|
||||
@@ -87,18 +88,18 @@ export class UpgradeStatusCommand extends CommandRunner {
|
||||
requestedWorkspaceIds,
|
||||
);
|
||||
|
||||
const groupedWorkspaceStatuses =
|
||||
this.groupWorkspaceStatusesByHealth(workspaceStatuses);
|
||||
const groupedWorkspaceUpgradeStatuses =
|
||||
this.groupWorkspaceUpgradeStatusesByHealth(workspaceStatuses);
|
||||
|
||||
lines.push(
|
||||
...this.formatWorkspaceStatuses(
|
||||
groupedWorkspaceStatuses,
|
||||
...this.formatWorkspaceUpgradeStatuses(
|
||||
groupedWorkspaceUpgradeStatuses,
|
||||
options.failedOnly,
|
||||
),
|
||||
);
|
||||
|
||||
lines.push(
|
||||
...this.formatSummary(instanceStatus, groupedWorkspaceStatuses),
|
||||
...this.formatSummary(instanceStatus, groupedWorkspaceUpgradeStatuses),
|
||||
);
|
||||
|
||||
console.log(lines.join('\n'));
|
||||
@@ -115,7 +116,7 @@ export class UpgradeStatusCommand extends CommandRunner {
|
||||
return ['', chalk.bold(`APP_VERSION: ${appVersion}`), ''];
|
||||
}
|
||||
|
||||
private formatInstanceStatus(status: MigrationCursorStatus): string[] {
|
||||
private formatInstanceStatus(status: InstanceUpgradeStatus): string[] {
|
||||
return [
|
||||
chalk.bold.underline('Instance'),
|
||||
...this.formatCursorStatus(status),
|
||||
@@ -123,8 +124,8 @@ export class UpgradeStatusCommand extends CommandRunner {
|
||||
];
|
||||
}
|
||||
|
||||
private formatWorkspaceStatuses(
|
||||
{ upToDate, behind, failed }: GroupedWorkspaceStatuses,
|
||||
private formatWorkspaceUpgradeStatuses(
|
||||
{ upToDate, behind, failed }: GroupedWorkspaceUpgradeStatuses,
|
||||
failedOnly?: boolean,
|
||||
): string[] {
|
||||
const lines: string[] = [chalk.bold.underline('Workspace')];
|
||||
@@ -137,19 +138,22 @@ export class UpgradeStatusCommand extends CommandRunner {
|
||||
|
||||
if (!failedOnly) {
|
||||
for (const workspaceStatus of upToDate) {
|
||||
lines.push(...this.formatWorkspaceStatus(workspaceStatus));
|
||||
lines.push(...this.formatWorkspaceUpgradeStatus(workspaceStatus));
|
||||
}
|
||||
}
|
||||
|
||||
for (const workspaceStatus of behind) {
|
||||
lines.push(...this.formatWorkspaceStatus(workspaceStatus));
|
||||
lines.push(...this.formatWorkspaceUpgradeStatus(workspaceStatus));
|
||||
}
|
||||
|
||||
if (failed.length > 0) {
|
||||
const groupedByCommand = new Map<string, WorkspaceStatus[]>();
|
||||
const groupedByCommand = new Map<
|
||||
string | null,
|
||||
WorkspaceUpgradeStatus[]
|
||||
>();
|
||||
|
||||
for (const workspaceStatus of failed) {
|
||||
const commandName = workspaceStatus.latestCommand?.name ?? 'unknown';
|
||||
const commandName = workspaceStatus.latestCommand?.name ?? null;
|
||||
|
||||
if (!groupedByCommand.has(commandName)) {
|
||||
groupedByCommand.set(commandName, []);
|
||||
@@ -159,10 +163,16 @@ export class UpgradeStatusCommand extends CommandRunner {
|
||||
}
|
||||
|
||||
for (const [commandName, statuses] of groupedByCommand) {
|
||||
lines.push(chalk.red.bold(` Failed at: ${commandName}`));
|
||||
const formattedCommandName = commandName
|
||||
? formatUpgradeCommandName(commandName)
|
||||
: 'unknown';
|
||||
|
||||
lines.push(chalk.red.bold(` Failed at: ${formattedCommandName}`));
|
||||
|
||||
for (const workspaceStatus of statuses) {
|
||||
lines.push(...this.formatWorkspaceStatus(workspaceStatus, true));
|
||||
lines.push(
|
||||
...this.formatWorkspaceUpgradeStatus(workspaceStatus, true),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,8 +180,8 @@ export class UpgradeStatusCommand extends CommandRunner {
|
||||
return lines;
|
||||
}
|
||||
|
||||
private formatWorkspaceStatus(
|
||||
status: WorkspaceStatus,
|
||||
private formatWorkspaceUpgradeStatus(
|
||||
status: WorkspaceUpgradeStatus,
|
||||
nested = false,
|
||||
): string[] {
|
||||
const baseIndent = nested ? ' ' : ' ';
|
||||
@@ -188,7 +198,7 @@ export class UpgradeStatusCommand extends CommandRunner {
|
||||
}
|
||||
|
||||
private formatCursorStatus(
|
||||
status: MigrationCursorStatus,
|
||||
status: InstanceUpgradeStatus,
|
||||
indent = ' ',
|
||||
): string[] {
|
||||
if (!status.latestCommand) {
|
||||
@@ -199,7 +209,7 @@ export class UpgradeStatusCommand extends CommandRunner {
|
||||
|
||||
const lines: string[] = [
|
||||
`${indent}Inferred version: ${status.inferredVersion ?? chalk.dim('unknown')}`,
|
||||
`${indent}Latest command: ${latestCommand.name}`,
|
||||
`${indent}Latest command: ${formatUpgradeCommandName(latestCommand.name)}`,
|
||||
`${indent}Status: ${HEALTH_LABELS[status.health]}`,
|
||||
`${indent}Executed by: ${latestCommand.executedByVersion}`,
|
||||
`${indent}At: ${latestCommand.createdAt.toISOString()}`,
|
||||
@@ -215,8 +225,8 @@ export class UpgradeStatusCommand extends CommandRunner {
|
||||
}
|
||||
|
||||
private formatSummary(
|
||||
instanceStatus: MigrationCursorStatus,
|
||||
{ upToDate, behind, failed }: GroupedWorkspaceStatuses,
|
||||
instanceStatus: InstanceUpgradeStatus,
|
||||
{ upToDate, behind, failed }: GroupedWorkspaceUpgradeStatuses,
|
||||
): string[] {
|
||||
const lines: string[] = [chalk.bold.underline('Summary')];
|
||||
const totalCount = upToDate.length + behind.length + failed.length;
|
||||
@@ -238,24 +248,30 @@ export class UpgradeStatusCommand extends CommandRunner {
|
||||
lines.push(` Workspaces: ${parts.join(', ')} (${totalCount} total)`);
|
||||
|
||||
if (behind.length > 0) {
|
||||
const behindCounts = new Map<string, number>();
|
||||
const behindCounts = new Map<string | null, number>();
|
||||
|
||||
for (const status of behind) {
|
||||
const commandName = status.latestCommand?.name ?? 'no commands';
|
||||
const commandName = status.latestCommand?.name ?? null;
|
||||
|
||||
behindCounts.set(commandName, (behindCounts.get(commandName) ?? 0) + 1);
|
||||
}
|
||||
|
||||
for (const [commandName, count] of behindCounts) {
|
||||
lines.push(chalk.yellow(` ${count} behind at: ${commandName}`));
|
||||
const formattedCommandName = commandName
|
||||
? formatUpgradeCommandName(commandName)
|
||||
: 'no commands';
|
||||
|
||||
lines.push(
|
||||
chalk.yellow(` ${count} behind at: ${formattedCommandName}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed.length > 0) {
|
||||
const failureCounts = new Map<string, number>();
|
||||
const failureCounts = new Map<string | null, number>();
|
||||
|
||||
for (const status of failed) {
|
||||
const commandName = status.latestCommand?.name ?? 'unknown';
|
||||
const commandName = status.latestCommand?.name ?? null;
|
||||
|
||||
failureCounts.set(
|
||||
commandName,
|
||||
@@ -264,7 +280,13 @@ export class UpgradeStatusCommand extends CommandRunner {
|
||||
}
|
||||
|
||||
for (const [commandName, count] of failureCounts) {
|
||||
lines.push(chalk.red(` ${count} failed at: ${commandName}`));
|
||||
const formattedCommandName = commandName
|
||||
? formatUpgradeCommandName(commandName)
|
||||
: 'unknown';
|
||||
|
||||
lines.push(
|
||||
chalk.red(` ${count} failed at: ${formattedCommandName}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,22 +295,22 @@ export class UpgradeStatusCommand extends CommandRunner {
|
||||
return lines;
|
||||
}
|
||||
|
||||
private groupWorkspaceStatusesByHealth(
|
||||
workspaceStatuses: WorkspaceStatus[],
|
||||
): GroupedWorkspaceStatuses {
|
||||
const upToDate: WorkspaceStatus[] = [];
|
||||
const behind: WorkspaceStatus[] = [];
|
||||
const failed: WorkspaceStatus[] = [];
|
||||
private groupWorkspaceUpgradeStatusesByHealth(
|
||||
workspaceStatuses: WorkspaceUpgradeStatus[],
|
||||
): GroupedWorkspaceUpgradeStatuses {
|
||||
const upToDate: WorkspaceUpgradeStatus[] = [];
|
||||
const behind: WorkspaceUpgradeStatus[] = [];
|
||||
const failed: WorkspaceUpgradeStatus[] = [];
|
||||
|
||||
for (const status of workspaceStatuses) {
|
||||
switch (status.health) {
|
||||
case 'up-to-date':
|
||||
case UpgradeHealthEnum.UP_TO_DATE:
|
||||
upToDate.push(status);
|
||||
break;
|
||||
case 'behind':
|
||||
case UpgradeHealthEnum.BEHIND:
|
||||
behind.push(status);
|
||||
break;
|
||||
case 'failed':
|
||||
case UpgradeHealthEnum.FAILED:
|
||||
failed.push(status);
|
||||
break;
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { InstanceUpgradeStatusDTO } from 'src/engine/core-modules/upgrade/dtos/instance-upgrade-status.dto';
|
||||
import { WorkspaceUpgradeRefDTO } from 'src/engine/core-modules/upgrade/dtos/workspace-upgrade-ref.dto';
|
||||
|
||||
@ObjectType('InstanceAndAllWorkspacesUpgradeStatus')
|
||||
export class InstanceAndAllWorkspacesUpgradeStatusDTO {
|
||||
@Field(() => InstanceUpgradeStatusDTO)
|
||||
instanceUpgradeStatus: InstanceUpgradeStatusDTO;
|
||||
|
||||
@Field(() => [WorkspaceUpgradeRefDTO])
|
||||
workspacesBehind: WorkspaceUpgradeRefDTO[];
|
||||
|
||||
@Field(() => [WorkspaceUpgradeRefDTO])
|
||||
workspacesFailed: WorkspaceUpgradeRefDTO[];
|
||||
|
||||
@Field(() => Date)
|
||||
computedAt: Date;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { UpgradeHealthEnum } from 'src/engine/core-modules/upgrade/dtos/upgrade-health.enum';
|
||||
import { type UpgradeMigrationStatus } from 'src/engine/core-modules/upgrade/upgrade-migration.entity';
|
||||
|
||||
@ObjectType('LatestUpgradeCommand')
|
||||
export class LatestUpgradeCommandDTO {
|
||||
@Field(() => String)
|
||||
name: string;
|
||||
|
||||
@Field(() => String)
|
||||
status: UpgradeMigrationStatus;
|
||||
|
||||
@Field(() => String)
|
||||
executedByVersion: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
errorMessage: string | null;
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
@ObjectType('InstanceUpgradeStatus')
|
||||
export class InstanceUpgradeStatusDTO {
|
||||
@Field(() => String, { nullable: true })
|
||||
inferredVersion: string | null;
|
||||
|
||||
@Field(() => UpgradeHealthEnum)
|
||||
health: UpgradeHealthEnum;
|
||||
|
||||
@Field(() => LatestUpgradeCommandDTO, { nullable: true })
|
||||
latestCommand: LatestUpgradeCommandDTO | null;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { UpgradeHealthEnum } from 'twenty-shared/types';
|
||||
|
||||
export { UpgradeHealthEnum };
|
||||
|
||||
registerEnumType(UpgradeHealthEnum, {
|
||||
name: 'UpgradeHealth',
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('WorkspaceUpgradeRef')
|
||||
export class WorkspaceUpgradeRefDTO {
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
name: string | null;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { LatestUpgradeCommandDTO } from 'src/engine/core-modules/upgrade/dtos/instance-upgrade-status.dto';
|
||||
import { UpgradeHealthEnum } from 'src/engine/core-modules/upgrade/dtos/upgrade-health.enum';
|
||||
|
||||
@ObjectType('WorkspaceUpgradeStatus')
|
||||
export class WorkspaceUpgradeStatusDTO {
|
||||
@Field(() => UUIDScalarType)
|
||||
workspaceId: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
displayName: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
inferredVersion: string | null;
|
||||
|
||||
@Field(() => UpgradeHealthEnum)
|
||||
health: UpgradeHealthEnum;
|
||||
|
||||
@Field(() => LatestUpgradeCommandDTO, { nullable: true })
|
||||
latestCommand: LatestUpgradeCommandDTO | null;
|
||||
}
|
||||
+209
-13
@@ -1,8 +1,13 @@
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { UpgradeHealthEnum } from 'twenty-shared/types';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
|
||||
import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service';
|
||||
import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/services/upgrade-migration.service';
|
||||
import { UpgradeSequenceReaderService } from 'src/engine/core-modules/upgrade/services/upgrade-sequence-reader.service';
|
||||
import { UpgradeStatusCacheService } from 'src/engine/core-modules/upgrade/services/upgrade-status-cache.service';
|
||||
import { UpgradeStatusService } from 'src/engine/core-modules/upgrade/services/upgrade-status.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@@ -16,16 +21,61 @@ const MOCK_SEQUENCE = [
|
||||
{ kind: 'workspace', name: LAST_WORKSPACE_COMMAND },
|
||||
];
|
||||
|
||||
type WorkspaceRecord = {
|
||||
id: string;
|
||||
displayName: string | null;
|
||||
};
|
||||
|
||||
const buildWorkspaceCacheGetMock = (
|
||||
workspaces: WorkspaceRecord[],
|
||||
): jest.Mock => {
|
||||
const byId = new Map(
|
||||
workspaces.map((workspace) => [workspace.id, workspace]),
|
||||
);
|
||||
|
||||
return jest.fn(async (_cacheKey: string, workspaceId: string) => {
|
||||
const workspace = byId.get(workspaceId);
|
||||
|
||||
if (!workspace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
...workspace,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
describe('UpgradeStatusService', () => {
|
||||
let service: UpgradeStatusService;
|
||||
let getLastAttemptedInstanceCommand: jest.Mock;
|
||||
let getWorkspaceLastAttemptedCommandName: jest.Mock;
|
||||
let workspaceFind: jest.Mock;
|
||||
let coreEntityCacheGet: jest.Mock;
|
||||
let cacheGetComputedAt: jest.Mock;
|
||||
let cacheGetBehindWorkspaceIds: jest.Mock;
|
||||
let cacheGetFailedWorkspaceIds: jest.Mock;
|
||||
let cacheWrite: jest.Mock;
|
||||
let cacheInvalidate: jest.Mock;
|
||||
|
||||
const mockActiveWorkspaces = (workspaces: WorkspaceRecord[]) => {
|
||||
workspaceFind.mockResolvedValue(workspaces);
|
||||
coreEntityCacheGet.mockImplementation(
|
||||
buildWorkspaceCacheGetMock(workspaces),
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
getLastAttemptedInstanceCommand = jest.fn();
|
||||
getWorkspaceLastAttemptedCommandName = jest.fn();
|
||||
workspaceFind = jest.fn();
|
||||
workspaceFind = jest.fn().mockResolvedValue([]);
|
||||
coreEntityCacheGet = jest.fn().mockResolvedValue(null);
|
||||
cacheGetComputedAt = jest.fn();
|
||||
cacheGetBehindWorkspaceIds = jest.fn().mockResolvedValue([]);
|
||||
cacheGetFailedWorkspaceIds = jest.fn().mockResolvedValue([]);
|
||||
cacheWrite = jest.fn().mockResolvedValue(undefined);
|
||||
cacheInvalidate = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
@@ -47,6 +97,20 @@ describe('UpgradeStatusService', () => {
|
||||
provide: getRepositoryToken(WorkspaceEntity),
|
||||
useValue: { find: workspaceFind },
|
||||
},
|
||||
{
|
||||
provide: CoreEntityCacheService,
|
||||
useValue: { get: coreEntityCacheGet },
|
||||
},
|
||||
{
|
||||
provide: UpgradeStatusCacheService,
|
||||
useValue: {
|
||||
getComputedAt: cacheGetComputedAt,
|
||||
getBehindWorkspaceIds: cacheGetBehindWorkspaceIds,
|
||||
getFailedWorkspaceIds: cacheGetFailedWorkspaceIds,
|
||||
write: cacheWrite,
|
||||
invalidate: cacheInvalidate,
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -65,7 +129,7 @@ describe('UpgradeStatusService', () => {
|
||||
|
||||
const result = await service.getInstanceStatus();
|
||||
|
||||
expect(result.health).toBe('up-to-date');
|
||||
expect(result.health).toBe(UpgradeHealthEnum.UP_TO_DATE);
|
||||
expect(result.inferredVersion).toBe('1.23.0');
|
||||
});
|
||||
|
||||
@@ -80,7 +144,7 @@ describe('UpgradeStatusService', () => {
|
||||
|
||||
const result = await service.getInstanceStatus();
|
||||
|
||||
expect(result.health).toBe('behind');
|
||||
expect(result.health).toBe(UpgradeHealthEnum.BEHIND);
|
||||
expect(result.inferredVersion).toBe('1.22.0');
|
||||
});
|
||||
|
||||
@@ -95,7 +159,7 @@ describe('UpgradeStatusService', () => {
|
||||
|
||||
const result = await service.getInstanceStatus();
|
||||
|
||||
expect(result.health).toBe('failed');
|
||||
expect(result.health).toBe(UpgradeHealthEnum.FAILED);
|
||||
expect(result.latestCommand?.errorMessage).toBe('column does not exist');
|
||||
});
|
||||
|
||||
@@ -104,7 +168,7 @@ describe('UpgradeStatusService', () => {
|
||||
|
||||
const result = await service.getInstanceStatus();
|
||||
|
||||
expect(result.health).toBe('behind');
|
||||
expect(result.health).toBe(UpgradeHealthEnum.BEHIND);
|
||||
expect(result.inferredVersion).toBeNull();
|
||||
expect(result.latestCommand).toBeNull();
|
||||
});
|
||||
@@ -112,7 +176,7 @@ describe('UpgradeStatusService', () => {
|
||||
|
||||
describe('getWorkspaceStatuses', () => {
|
||||
it('should return up-to-date for workspace at last command', async () => {
|
||||
workspaceFind.mockResolvedValue([{ id: 'ws-1', displayName: 'Apple' }]);
|
||||
mockActiveWorkspaces([{ id: 'ws-1', displayName: 'Apple' }]);
|
||||
|
||||
getWorkspaceLastAttemptedCommandName.mockResolvedValue(
|
||||
new Map([
|
||||
@@ -133,11 +197,11 @@ describe('UpgradeStatusService', () => {
|
||||
const results = await service.getWorkspaceStatuses();
|
||||
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].health).toBe('up-to-date');
|
||||
expect(results[0].health).toBe(UpgradeHealthEnum.UP_TO_DATE);
|
||||
});
|
||||
|
||||
it('should return behind for workspace not at last command', async () => {
|
||||
workspaceFind.mockResolvedValue([
|
||||
mockActiveWorkspaces([
|
||||
{ id: 'ws-1', displayName: 'Apple' },
|
||||
{ id: 'ws-2', displayName: 'Google' },
|
||||
]);
|
||||
@@ -172,24 +236,24 @@ describe('UpgradeStatusService', () => {
|
||||
const results = await service.getWorkspaceStatuses();
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0].health).toBe('up-to-date');
|
||||
expect(results[1].health).toBe('behind');
|
||||
expect(results[0].health).toBe(UpgradeHealthEnum.UP_TO_DATE);
|
||||
expect(results[1].health).toBe(UpgradeHealthEnum.BEHIND);
|
||||
});
|
||||
|
||||
it('should return behind for workspace with no migration history', async () => {
|
||||
workspaceFind.mockResolvedValue([{ id: 'ws-1', displayName: 'Apple' }]);
|
||||
mockActiveWorkspaces([{ id: 'ws-1', displayName: 'Apple' }]);
|
||||
|
||||
getWorkspaceLastAttemptedCommandName.mockResolvedValue(new Map());
|
||||
|
||||
const results = await service.getWorkspaceStatuses();
|
||||
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].health).toBe('behind');
|
||||
expect(results[0].health).toBe(UpgradeHealthEnum.BEHIND);
|
||||
expect(results[0].latestCommand).toBeNull();
|
||||
});
|
||||
|
||||
it('should return empty array when no workspaces exist', async () => {
|
||||
workspaceFind.mockResolvedValue([]);
|
||||
mockActiveWorkspaces([]);
|
||||
getWorkspaceLastAttemptedCommandName.mockResolvedValue(new Map());
|
||||
|
||||
const results = await service.getWorkspaceStatuses();
|
||||
@@ -197,4 +261,136 @@ describe('UpgradeStatusService', () => {
|
||||
expect(results).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInstanceAndAllWorkspacesStatus', () => {
|
||||
it('should hydrate cached behind/failed ids with display names without calling getWorkspaceStatuses', async () => {
|
||||
const computedAt = new Date('2025-06-02T10:00:00Z');
|
||||
|
||||
cacheGetComputedAt.mockResolvedValue(computedAt);
|
||||
cacheGetBehindWorkspaceIds.mockResolvedValue(['ws-2']);
|
||||
cacheGetFailedWorkspaceIds.mockResolvedValue(['ws-3']);
|
||||
getLastAttemptedInstanceCommand.mockResolvedValue({
|
||||
name: LAST_INSTANCE_COMMAND,
|
||||
status: 'completed',
|
||||
executedByVersion: '1.23.0',
|
||||
errorMessage: null,
|
||||
createdAt: new Date('2025-06-01T00:00:00Z'),
|
||||
});
|
||||
coreEntityCacheGet.mockImplementation(
|
||||
buildWorkspaceCacheGetMock([
|
||||
{ id: 'ws-2', displayName: 'Banana' },
|
||||
{ id: 'ws-3', displayName: 'Cherry' },
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await service.getInstanceAndAllWorkspacesStatus();
|
||||
|
||||
expect(result.workspacesBehind).toEqual([{ id: 'ws-2', name: 'Banana' }]);
|
||||
expect(result.workspacesFailed).toEqual([{ id: 'ws-3', name: 'Cherry' }]);
|
||||
expect(result.computedAt).toEqual(computedAt);
|
||||
expect(getWorkspaceLastAttemptedCommandName).not.toHaveBeenCalled();
|
||||
expect(cacheWrite).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fall back to a refresh when the cache marker is missing', async () => {
|
||||
cacheGetComputedAt.mockResolvedValue(null);
|
||||
getLastAttemptedInstanceCommand.mockResolvedValue(null);
|
||||
mockActiveWorkspaces([{ id: 'ws-1', displayName: 'Apple' }]);
|
||||
getWorkspaceLastAttemptedCommandName.mockResolvedValue(new Map());
|
||||
|
||||
const result = await service.getInstanceAndAllWorkspacesStatus();
|
||||
|
||||
expect(cacheWrite).toHaveBeenCalledTimes(1);
|
||||
expect(result.workspacesBehind).toEqual([{ id: 'ws-1', name: 'Apple' }]);
|
||||
});
|
||||
|
||||
it('should use null name when a cached id is missing from the cache', async () => {
|
||||
cacheGetComputedAt.mockResolvedValue(new Date());
|
||||
cacheGetBehindWorkspaceIds.mockResolvedValue(['ws-orphan']);
|
||||
getLastAttemptedInstanceCommand.mockResolvedValue(null);
|
||||
coreEntityCacheGet.mockResolvedValue(null);
|
||||
|
||||
const result = await service.getInstanceAndAllWorkspacesStatus();
|
||||
|
||||
expect(result.workspacesBehind).toEqual([
|
||||
{ id: 'ws-orphan', name: null },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not query workspace names when both cached id sets are empty', async () => {
|
||||
cacheGetComputedAt.mockResolvedValue(new Date());
|
||||
getLastAttemptedInstanceCommand.mockResolvedValue(null);
|
||||
|
||||
await service.getInstanceAndAllWorkspacesStatus();
|
||||
|
||||
expect(coreEntityCacheGet).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshInstanceAndAllWorkspacesStatus', () => {
|
||||
it('should partition workspaces by health, write to cache, and return the fresh payload', async () => {
|
||||
getLastAttemptedInstanceCommand.mockResolvedValue(null);
|
||||
mockActiveWorkspaces([
|
||||
{ id: 'ws-1', displayName: 'Apple' },
|
||||
{ id: 'ws-2', displayName: 'Banana' },
|
||||
{ id: 'ws-3', displayName: 'Cherry' },
|
||||
]);
|
||||
getWorkspaceLastAttemptedCommandName.mockResolvedValue(
|
||||
new Map([
|
||||
[
|
||||
'ws-1',
|
||||
{
|
||||
workspaceId: 'ws-1',
|
||||
name: LAST_WORKSPACE_COMMAND,
|
||||
status: 'completed',
|
||||
executedByVersion: '1.23.0',
|
||||
errorMessage: null,
|
||||
createdAt: new Date('2025-06-01T00:00:00Z'),
|
||||
},
|
||||
],
|
||||
[
|
||||
'ws-2',
|
||||
{
|
||||
workspaceId: 'ws-2',
|
||||
name: EARLIER_COMMAND,
|
||||
status: 'completed',
|
||||
executedByVersion: '1.22.0',
|
||||
errorMessage: null,
|
||||
createdAt: new Date('2025-05-01T00:00:00Z'),
|
||||
},
|
||||
],
|
||||
[
|
||||
'ws-3',
|
||||
{
|
||||
workspaceId: 'ws-3',
|
||||
name: LAST_WORKSPACE_COMMAND,
|
||||
status: 'failed',
|
||||
executedByVersion: '1.23.0',
|
||||
errorMessage: 'boom',
|
||||
createdAt: new Date('2025-06-01T00:00:00Z'),
|
||||
},
|
||||
],
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await service.refreshInstanceAndAllWorkspacesStatus();
|
||||
|
||||
expect(result.workspacesBehind).toEqual([{ id: 'ws-2', name: 'Banana' }]);
|
||||
expect(result.workspacesFailed).toEqual([{ id: 'ws-3', name: 'Cherry' }]);
|
||||
|
||||
expect(cacheWrite).toHaveBeenCalledWith({
|
||||
behindWorkspaceIds: ['ws-2'],
|
||||
failedWorkspaceIds: ['ws-3'],
|
||||
computedAt: expect.any(Date),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalidateInstanceAndAllWorkspacesStatus', () => {
|
||||
it('should delegate to the cache service', async () => {
|
||||
await service.invalidateInstanceAndAllWorkspacesStatus();
|
||||
|
||||
expect(cacheInvalidate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+20
-3
@@ -7,6 +7,7 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
|
||||
import { type FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
import { type SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';
|
||||
import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/services/upgrade-migration.service';
|
||||
import { UpgradeStatusService } from 'src/engine/core-modules/upgrade/services/upgrade-status.service';
|
||||
import { WorkspaceVersionService } from 'src/engine/workspace-manager/workspace-version/services/workspace-version.service';
|
||||
|
||||
type RunSingleMigrationResult =
|
||||
@@ -24,6 +25,7 @@ export class InstanceCommandRunnerService {
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly upgradeMigrationService: UpgradeMigrationService,
|
||||
private readonly workspaceVersionService: WorkspaceVersionService,
|
||||
private readonly upgradeStatusService: UpgradeStatusService,
|
||||
) {}
|
||||
|
||||
async runFastInstanceCommand({
|
||||
@@ -71,6 +73,10 @@ export class InstanceCommandRunnerService {
|
||||
});
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
this.logger.log(`${name} executed successfully`);
|
||||
|
||||
return { status: 'success' };
|
||||
} catch (error) {
|
||||
if (queryRunner.isTransactionActive) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
@@ -96,11 +102,20 @@ export class InstanceCommandRunnerService {
|
||||
return { status: 'failed', error };
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
await this.safeInvalidateUpgradeStatusCache();
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`${name} executed successfully`);
|
||||
|
||||
return { status: 'success' };
|
||||
private async safeInvalidateUpgradeStatusCache(): Promise<void> {
|
||||
try {
|
||||
await this.upgradeStatusService.invalidateInstanceAndAllWorkspacesStatus();
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Failed to invalidate upgrade-status cache: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async runSlowInstanceCommand({
|
||||
@@ -150,6 +165,8 @@ export class InstanceCommandRunnerService {
|
||||
error instanceof Error ? error.stack : String(error),
|
||||
);
|
||||
|
||||
await this.safeInvalidateUpgradeStatusCache();
|
||||
|
||||
return { status: 'failed', error };
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { In, IsNull, type QueryRunner, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
UpgradeMigrationEntity,
|
||||
type UpgradeMigrationStatus,
|
||||
UpgradeMigrationStatus,
|
||||
} from 'src/engine/core-modules/upgrade/upgrade-migration.entity';
|
||||
import { formatUpgradeErrorForStorage } from 'src/engine/core-modules/upgrade/utils/format-upgrade-error-for-storage.util';
|
||||
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
|
||||
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
|
||||
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
||||
|
||||
const BEHIND_IDS_KEY = 'upgrade-status:behind-workspace-ids';
|
||||
const FAILED_IDS_KEY = 'upgrade-status:failed-workspace-ids';
|
||||
const COMPUTED_AT_KEY = 'upgrade-status:computed-at';
|
||||
|
||||
const CACHE_TTL_MS = 60 * 60 * 1000;
|
||||
@Injectable()
|
||||
export class UpgradeStatusCacheService {
|
||||
constructor(
|
||||
@InjectCacheStorage(CacheStorageNamespace.EngineHealth)
|
||||
private readonly cacheStorage: CacheStorageService,
|
||||
) {}
|
||||
|
||||
async getComputedAt(): Promise<Date | null> {
|
||||
const computedAt = await this.cacheStorage.get<string>(COMPUTED_AT_KEY);
|
||||
|
||||
return isDefined(computedAt) ? new Date(computedAt) : null;
|
||||
}
|
||||
|
||||
async getBehindWorkspaceIds(): Promise<string[]> {
|
||||
return this.cacheStorage.setMembers(BEHIND_IDS_KEY);
|
||||
}
|
||||
|
||||
async getFailedWorkspaceIds(): Promise<string[]> {
|
||||
return this.cacheStorage.setMembers(FAILED_IDS_KEY);
|
||||
}
|
||||
|
||||
async write({
|
||||
behindWorkspaceIds,
|
||||
failedWorkspaceIds,
|
||||
computedAt,
|
||||
}: {
|
||||
behindWorkspaceIds: string[];
|
||||
failedWorkspaceIds: string[];
|
||||
computedAt: Date;
|
||||
}): Promise<void> {
|
||||
await Promise.all([
|
||||
this.cacheStorage.del(BEHIND_IDS_KEY),
|
||||
this.cacheStorage.del(FAILED_IDS_KEY),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
this.cacheStorage.setAdd(
|
||||
BEHIND_IDS_KEY,
|
||||
behindWorkspaceIds,
|
||||
CACHE_TTL_MS,
|
||||
),
|
||||
this.cacheStorage.setAdd(
|
||||
FAILED_IDS_KEY,
|
||||
failedWorkspaceIds,
|
||||
CACHE_TTL_MS,
|
||||
),
|
||||
this.cacheStorage.set(
|
||||
COMPUTED_AT_KEY,
|
||||
computedAt.toISOString(),
|
||||
CACHE_TTL_MS,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
async invalidate(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.cacheStorage.del(BEHIND_IDS_KEY),
|
||||
this.cacheStorage.del(FAILED_IDS_KEY),
|
||||
this.cacheStorage.del(COMPUTED_AT_KEY),
|
||||
]);
|
||||
}
|
||||
}
|
||||
+163
-32
@@ -1,50 +1,69 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { UpgradeHealthEnum } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service';
|
||||
import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/services/upgrade-migration.service';
|
||||
import { UpgradeSequenceReaderService } from 'src/engine/core-modules/upgrade/services/upgrade-sequence-reader.service';
|
||||
import { UpgradeStatusCacheService } from 'src/engine/core-modules/upgrade/services/upgrade-status-cache.service';
|
||||
import { type UpgradeMigrationStatus } from 'src/engine/core-modules/upgrade/upgrade-migration.entity';
|
||||
import { extractVersionFromCommandName } from 'src/engine/core-modules/upgrade/utils/extract-version-from-command-name.util';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
export type UpgradeHealth = 'up-to-date' | 'behind' | 'failed';
|
||||
|
||||
export type MigrationCursorStatus = {
|
||||
inferredVersion: string | null;
|
||||
health: UpgradeHealth;
|
||||
latestCommand: {
|
||||
name: string;
|
||||
status: UpgradeMigrationStatus;
|
||||
executedByVersion: string;
|
||||
errorMessage: string | null;
|
||||
createdAt: Date;
|
||||
} | null;
|
||||
export type LatestUpgradeCommand = {
|
||||
name: string;
|
||||
status: UpgradeMigrationStatus;
|
||||
executedByVersion: string;
|
||||
errorMessage: string | null;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
export type WorkspaceStatus = MigrationCursorStatus & {
|
||||
export type InstanceUpgradeStatus = {
|
||||
inferredVersion: string | null;
|
||||
health: UpgradeHealthEnum;
|
||||
latestCommand: LatestUpgradeCommand | null;
|
||||
};
|
||||
|
||||
export type WorkspaceUpgradeStatus = {
|
||||
workspaceId: string;
|
||||
displayName: string | null;
|
||||
inferredVersion: string | null;
|
||||
health: UpgradeHealthEnum;
|
||||
latestCommand: LatestUpgradeCommand | null;
|
||||
};
|
||||
|
||||
export type WorkspaceUpgradeRef = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
};
|
||||
|
||||
export type InstanceAndAllWorkspacesUpgradeStatus = {
|
||||
instanceUpgradeStatus: InstanceUpgradeStatus;
|
||||
workspacesBehind: WorkspaceUpgradeRef[];
|
||||
workspacesFailed: WorkspaceUpgradeRef[];
|
||||
computedAt: Date;
|
||||
};
|
||||
|
||||
const deriveHealth = (
|
||||
migration: { name: string; status: UpgradeMigrationStatus },
|
||||
lastExpectedCommandName: string | null,
|
||||
): UpgradeHealth => {
|
||||
): UpgradeHealthEnum => {
|
||||
if (migration.status === 'failed') {
|
||||
return 'failed';
|
||||
return UpgradeHealthEnum.FAILED;
|
||||
}
|
||||
|
||||
if (
|
||||
lastExpectedCommandName !== null &&
|
||||
migration.name !== lastExpectedCommandName
|
||||
) {
|
||||
return 'behind';
|
||||
return UpgradeHealthEnum.BEHIND;
|
||||
}
|
||||
|
||||
return 'up-to-date';
|
||||
return UpgradeHealthEnum.UP_TO_DATE;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -56,9 +75,11 @@ export class UpgradeStatusService {
|
||||
private readonly upgradeSequenceReaderService: UpgradeSequenceReaderService,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
private readonly upgradeStatusCacheService: UpgradeStatusCacheService,
|
||||
private readonly coreEntityCacheService: CoreEntityCacheService,
|
||||
) {}
|
||||
|
||||
async getInstanceStatus(): Promise<MigrationCursorStatus> {
|
||||
async getInstanceStatus(): Promise<InstanceUpgradeStatus> {
|
||||
const migration =
|
||||
await this.upgradeMigrationService.getLastAttemptedInstanceCommand();
|
||||
|
||||
@@ -75,8 +96,9 @@ export class UpgradeStatusService {
|
||||
|
||||
async getWorkspaceStatuses(
|
||||
filterWorkspaceIds?: string[],
|
||||
): Promise<WorkspaceStatus[]> {
|
||||
const workspaces = await this.loadWorkspaces(filterWorkspaceIds);
|
||||
): Promise<WorkspaceUpgradeStatus[]> {
|
||||
const workspaces =
|
||||
await this.loadActiveOrSuspendedWorkspaces(filterWorkspaceIds);
|
||||
|
||||
if (filterWorkspaceIds) {
|
||||
const foundIds = new Set(workspaces.map((workspace) => workspace.id));
|
||||
@@ -110,18 +132,93 @@ export class UpgradeStatusService {
|
||||
}));
|
||||
}
|
||||
|
||||
async getInstanceAndAllWorkspacesStatus(): Promise<InstanceAndAllWorkspacesUpgradeStatus> {
|
||||
const computedAt = await this.upgradeStatusCacheService.getComputedAt();
|
||||
|
||||
if (!isDefined(computedAt)) {
|
||||
return this.refreshInstanceAndAllWorkspacesStatus();
|
||||
}
|
||||
|
||||
const [instanceUpgradeStatus, behindWorkspaceIds, failedWorkspaceIds] =
|
||||
await Promise.all([
|
||||
this.getInstanceStatus(),
|
||||
this.upgradeStatusCacheService.getBehindWorkspaceIds(),
|
||||
this.upgradeStatusCacheService.getFailedWorkspaceIds(),
|
||||
]);
|
||||
|
||||
const workspaceNamesById = await this.loadWorkspaceNamesById([
|
||||
...behindWorkspaceIds,
|
||||
...failedWorkspaceIds,
|
||||
]);
|
||||
|
||||
return {
|
||||
instanceUpgradeStatus,
|
||||
workspacesBehind: this.toWorkspaceRefs(
|
||||
behindWorkspaceIds,
|
||||
workspaceNamesById,
|
||||
),
|
||||
workspacesFailed: this.toWorkspaceRefs(
|
||||
failedWorkspaceIds,
|
||||
workspaceNamesById,
|
||||
),
|
||||
computedAt,
|
||||
};
|
||||
}
|
||||
|
||||
async refreshInstanceAndAllWorkspacesStatus(): Promise<InstanceAndAllWorkspacesUpgradeStatus> {
|
||||
this.logger.log('Recomputing upgrade status for all workspaces');
|
||||
|
||||
const [instanceUpgradeStatus, workspaceStatuses] = await Promise.all([
|
||||
this.getInstanceStatus(),
|
||||
this.getWorkspaceStatuses(),
|
||||
]);
|
||||
|
||||
const workspacesBehind: WorkspaceUpgradeRef[] = [];
|
||||
const workspacesFailed: WorkspaceUpgradeRef[] = [];
|
||||
|
||||
for (const workspaceStatus of workspaceStatuses) {
|
||||
const workspaceRef: WorkspaceUpgradeRef = {
|
||||
id: workspaceStatus.workspaceId,
|
||||
name: workspaceStatus.displayName,
|
||||
};
|
||||
|
||||
if (workspaceStatus.health === UpgradeHealthEnum.BEHIND) {
|
||||
workspacesBehind.push(workspaceRef);
|
||||
} else if (workspaceStatus.health === UpgradeHealthEnum.FAILED) {
|
||||
workspacesFailed.push(workspaceRef);
|
||||
}
|
||||
}
|
||||
|
||||
const computedAt = new Date();
|
||||
|
||||
await this.upgradeStatusCacheService.write({
|
||||
behindWorkspaceIds: workspacesBehind.map((workspace) => workspace.id),
|
||||
failedWorkspaceIds: workspacesFailed.map((workspace) => workspace.id),
|
||||
computedAt,
|
||||
});
|
||||
|
||||
return {
|
||||
instanceUpgradeStatus,
|
||||
workspacesBehind,
|
||||
workspacesFailed,
|
||||
computedAt,
|
||||
};
|
||||
}
|
||||
|
||||
async invalidateInstanceAndAllWorkspacesStatus(): Promise<void> {
|
||||
await this.upgradeStatusCacheService.invalidate();
|
||||
}
|
||||
|
||||
private buildCursorStatus(
|
||||
migration: {
|
||||
name: string;
|
||||
status: UpgradeMigrationStatus;
|
||||
executedByVersion: string;
|
||||
errorMessage: string | null;
|
||||
createdAt: Date;
|
||||
} | null,
|
||||
migration: LatestUpgradeCommand | null,
|
||||
lastExpectedCommandName: string | null,
|
||||
): MigrationCursorStatus {
|
||||
): InstanceUpgradeStatus {
|
||||
if (!migration) {
|
||||
return { inferredVersion: null, health: 'behind', latestCommand: null };
|
||||
return {
|
||||
inferredVersion: null,
|
||||
health: UpgradeHealthEnum.BEHIND,
|
||||
latestCommand: null,
|
||||
};
|
||||
}
|
||||
|
||||
const health = deriveHealth(migration, lastExpectedCommandName);
|
||||
@@ -139,7 +236,7 @@ export class UpgradeStatusService {
|
||||
};
|
||||
}
|
||||
|
||||
private async loadWorkspaces(
|
||||
private async loadActiveOrSuspendedWorkspaces(
|
||||
workspaceIds?: string[],
|
||||
): Promise<Pick<WorkspaceEntity, 'id' | 'displayName'>[]> {
|
||||
return this.workspaceRepository.find({
|
||||
@@ -156,4 +253,38 @@ export class UpgradeStatusService {
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
private async loadWorkspaceNamesById(
|
||||
workspaceIds: string[],
|
||||
): Promise<Map<string, string | null>> {
|
||||
const namesById = new Map<string, string | null>();
|
||||
|
||||
if (workspaceIds.length === 0) {
|
||||
return namesById;
|
||||
}
|
||||
|
||||
const workspaces = await Promise.all(
|
||||
workspaceIds.map((workspaceId) =>
|
||||
this.coreEntityCacheService.get('workspaceEntity', workspaceId),
|
||||
),
|
||||
);
|
||||
|
||||
for (const workspace of workspaces) {
|
||||
if (isDefined(workspace)) {
|
||||
namesById.set(workspace.id, workspace.displayName ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
return namesById;
|
||||
}
|
||||
|
||||
private toWorkspaceRefs(
|
||||
workspaceIds: string[],
|
||||
workspaceNamesById: Map<string, string | null>,
|
||||
): WorkspaceUpgradeRef[] {
|
||||
return workspaceIds.map((workspaceId) => ({
|
||||
id: workspaceId,
|
||||
name: workspaceNamesById.get(workspaceId) ?? null,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
+30
-10
@@ -5,6 +5,7 @@ import { type ParsedUpgradeCommandOptions } from 'src/database/commands/upgrade-
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { type RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/services/upgrade-command-registry.service';
|
||||
import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/services/upgrade-migration.service';
|
||||
import { UpgradeStatusService } from 'src/engine/core-modules/upgrade/services/upgrade-status.service';
|
||||
|
||||
type WorkspaceCommandEntry = Pick<
|
||||
RegisteredWorkspaceCommand,
|
||||
@@ -24,6 +25,7 @@ export class WorkspaceCommandRunnerService {
|
||||
constructor(
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly upgradeMigrationService: UpgradeMigrationService,
|
||||
private readonly upgradeStatusService: UpgradeStatusService,
|
||||
) {}
|
||||
|
||||
async runWorkspaceCommands({
|
||||
@@ -40,17 +42,35 @@ export class WorkspaceCommandRunnerService {
|
||||
const executedByVersion =
|
||||
this.twentyConfigService.get('APP_VERSION') ?? 'unknown';
|
||||
|
||||
for (const workspaceCommandEntry of workspaceCommands) {
|
||||
await this.runSingleWorkspaceCommandOrThrow({
|
||||
workspaceCommandEntry,
|
||||
workspaceId,
|
||||
executedByVersion,
|
||||
options,
|
||||
iteratorContext,
|
||||
});
|
||||
}
|
||||
try {
|
||||
for (const workspaceCommandEntry of workspaceCommands) {
|
||||
await this.runSingleWorkspaceCommandOrThrow({
|
||||
workspaceCommandEntry,
|
||||
workspaceId,
|
||||
executedByVersion,
|
||||
options,
|
||||
iteratorContext,
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log(`Upgrade for workspace ${workspaceId} completed.`);
|
||||
this.logger.log(`Upgrade for workspace ${workspaceId} completed.`);
|
||||
} finally {
|
||||
if (!options.dryRun) {
|
||||
await this.safeInvalidateWorkspace(workspaceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async safeInvalidateWorkspace(workspaceId: string): Promise<void> {
|
||||
try {
|
||||
await this.upgradeStatusService.invalidateInstanceAndAllWorkspacesStatus();
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Failed to invalidate upgrade-status cache for workspace ${workspaceId}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async runSingleWorkspaceCommandOrThrow({
|
||||
|
||||
@@ -22,6 +22,13 @@ export type UpgradeMigrationStatus = 'completed' | 'failed';
|
||||
unique: true,
|
||||
where: '"workspaceId" IS NOT NULL',
|
||||
})
|
||||
@Index(
|
||||
'IDX_UPGRADE_MIGRATION_WORKSPACE_ID_NAME_ATTEMPT',
|
||||
['workspaceId', 'name', 'attempt'],
|
||||
{
|
||||
where: '"workspaceId" IS NOT NULL',
|
||||
},
|
||||
)
|
||||
export class UpgradeMigrationEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@@ -5,11 +5,13 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
|
||||
import { InstanceCommandProviderModule } from 'src/database/commands/upgrade-version-command/instance-command-provider.module';
|
||||
import { WorkspaceCommandProviderModule } from 'src/database/commands/upgrade-version-command/workspace-command-provider.module';
|
||||
import { CoreEntityCacheModule } from 'src/engine/core-entity-cache/core-entity-cache.module';
|
||||
import { InstanceCommandRunnerService } from 'src/engine/core-modules/upgrade/services/instance-command-runner.service';
|
||||
import { UpgradeCommandRegistryService } from 'src/engine/core-modules/upgrade/services/upgrade-command-registry.service';
|
||||
import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/services/upgrade-migration.service';
|
||||
import { UpgradeSequenceReaderService } from 'src/engine/core-modules/upgrade/services/upgrade-sequence-reader.service';
|
||||
import { UpgradeSequenceRunnerService } from 'src/engine/core-modules/upgrade/services/upgrade-sequence-runner.service';
|
||||
import { UpgradeStatusCacheService } from 'src/engine/core-modules/upgrade/services/upgrade-status-cache.service';
|
||||
import { UpgradeStatusService } from 'src/engine/core-modules/upgrade/services/upgrade-status.service';
|
||||
import { WorkspaceCommandRunnerService } from 'src/engine/core-modules/upgrade/services/workspace-command-runner.service';
|
||||
import { UpgradeMigrationEntity } from 'src/engine/core-modules/upgrade/upgrade-migration.entity';
|
||||
@@ -18,6 +20,7 @@ import { WorkspaceVersionModule } from 'src/engine/workspace-manager/workspace-v
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CoreEntityCacheModule,
|
||||
DiscoveryModule,
|
||||
InstanceCommandProviderModule,
|
||||
WorkspaceCommandProviderModule,
|
||||
@@ -33,6 +36,7 @@ import { WorkspaceVersionModule } from 'src/engine/workspace-manager/workspace-v
|
||||
UpgradeSequenceReaderService,
|
||||
UpgradeSequenceRunnerService,
|
||||
UpgradeStatusService,
|
||||
UpgradeStatusCacheService,
|
||||
],
|
||||
exports: [
|
||||
UpgradeMigrationService,
|
||||
@@ -42,6 +46,7 @@ import { WorkspaceVersionModule } from 'src/engine/workspace-manager/workspace-v
|
||||
UpgradeSequenceReaderService,
|
||||
UpgradeSequenceRunnerService,
|
||||
UpgradeStatusService,
|
||||
UpgradeStatusCacheService,
|
||||
],
|
||||
})
|
||||
export class UpgradeModule {}
|
||||
|
||||
+9
@@ -14,6 +14,7 @@ import {
|
||||
type WorkspaceUpgradeStep,
|
||||
} from 'src/engine/core-modules/upgrade/services/upgrade-sequence-reader.service';
|
||||
import { UpgradeSequenceRunnerService } from 'src/engine/core-modules/upgrade/services/upgrade-sequence-runner.service';
|
||||
import { UpgradeStatusService } from 'src/engine/core-modules/upgrade/services/upgrade-status.service';
|
||||
import { WorkspaceCommandRunnerService } from 'src/engine/core-modules/upgrade/services/workspace-command-runner.service';
|
||||
import { UpgradeMigrationEntity } from 'src/engine/core-modules/upgrade/upgrade-migration.entity';
|
||||
import {
|
||||
@@ -141,6 +142,14 @@ export const createUpgradeSequenceRunnerIntegrationTestModule = async () => {
|
||||
provide: UpgradeSequenceReaderService,
|
||||
useFactory: () => new UpgradeSequenceReaderService({} as any),
|
||||
},
|
||||
{
|
||||
provide: UpgradeStatusService,
|
||||
useValue: {
|
||||
invalidateInstanceAndAllWorkspacesStatus: jest
|
||||
.fn()
|
||||
.mockResolvedValue(undefined),
|
||||
},
|
||||
},
|
||||
InstanceCommandRunnerService,
|
||||
WorkspaceCommandRunnerService,
|
||||
{
|
||||
|
||||
@@ -68,6 +68,9 @@ export enum SettingsPath {
|
||||
AdminPanelEnterprise = 'admin-panel#enterprise',
|
||||
AdminPanelHealthStatus = 'admin-panel#health-status',
|
||||
AdminPanelIndicatorHealthStatus = 'admin-panel/health-status/:indicatorId',
|
||||
AdminPanelInferredVersion = 'admin-panel/health-status/inferred-version',
|
||||
AdminPanelInstanceStatus = 'admin-panel/health-status/instance-status',
|
||||
AdminPanelWorkspacesStatus = 'admin-panel/health-status/workspaces-status',
|
||||
AdminPanelQueueDetail = 'admin-panel/health-status/queue/:queueName',
|
||||
AdminPanelConfigVariableDetails = 'admin-panel/config-variables/:variableName',
|
||||
AdminPanelNewAiProvider = 'admin-panel/ai/new-provider',
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum UpgradeHealthEnum {
|
||||
UP_TO_DATE = 'UP_TO_DATE',
|
||||
BEHIND = 'BEHIND',
|
||||
FAILED = 'FAILED',
|
||||
}
|
||||
@@ -278,6 +278,7 @@ export type {
|
||||
} from './StepFilters';
|
||||
export { StepLogicalOperator } from './StepFilters';
|
||||
export { TwoFactorAuthenticationStrategy } from './TwoFactorAuthenticationStrategy';
|
||||
export { UpgradeHealthEnum } from './UpgradeHealthEnum';
|
||||
export { IsValidGraphQLEnumName } from './validators/is-valid-graphql-enum-name.validator';
|
||||
export { ViewCalendarLayout } from './ViewCalendarLayout';
|
||||
export { ViewFilterGroupLogicalOperator } from './ViewFilterGroupLogicalOperator';
|
||||
|
||||
@@ -189,6 +189,7 @@ export { isMetadataGqlOperationSignature } from './typeguard/isMetadataGqlOperat
|
||||
export { isPlainObject } from './typeguard/isPlainObject';
|
||||
export { isRecordGqlOperationSignature } from './typeguard/isRecordGqlOperationSignature';
|
||||
export { throwIfNotDefined } from './typeguard/throwIfNotDefined';
|
||||
export { formatUpgradeCommandName } from './upgrade/formatUpgradeCommandName';
|
||||
export { absoluteUrlSchema } from './url/absoluteUrlSchema';
|
||||
export { buildSignedPath } from './url/buildSignedPath';
|
||||
export { ensureAbsoluteUrl } from './url/ensureAbsoluteUrl';
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { formatUpgradeCommandName } from '../formatUpgradeCommandName';
|
||||
|
||||
describe('formatUpgradeCommandName', () => {
|
||||
it('should append (instance fast) for fast instance commands', () => {
|
||||
expect(
|
||||
formatUpgradeCommandName(
|
||||
'1.23.0_DropWorkspaceVersionColumnFastInstanceCommand_1785000000000',
|
||||
),
|
||||
).toBe('DropWorkspaceVersionColumn 1785000000000 (1.23.0) (instance fast)');
|
||||
});
|
||||
|
||||
it('should append (instance slow) for slow instance commands', () => {
|
||||
expect(
|
||||
formatUpgradeCommandName(
|
||||
'1.23.0_BackfillWorkspaceIdSlowInstanceCommand_1785000000001',
|
||||
),
|
||||
).toBe('BackfillWorkspaceId 1785000000001 (1.23.0) (instance slow)');
|
||||
});
|
||||
|
||||
it('should append (workspace) for workspace commands', () => {
|
||||
expect(
|
||||
formatUpgradeCommandName(
|
||||
'1.22.0_BackfillStandardSkillsCommand_1780000002000',
|
||||
),
|
||||
).toBe('BackfillStandardSkills 1780000002000 (1.22.0) (workspace)');
|
||||
});
|
||||
|
||||
it('should return raw name when fewer than three segments', () => {
|
||||
expect(formatUpgradeCommandName('short')).toBe('short');
|
||||
expect(formatUpgradeCommandName('a_b')).toBe('a_b');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
const TRAILING_TIMESTAMP_PATTERN = /^\d+$/;
|
||||
|
||||
const getCommandKindLabel = (className: string): string => {
|
||||
if (className.endsWith('FastInstanceCommand')) {
|
||||
return '(instance fast)';
|
||||
}
|
||||
|
||||
if (className.endsWith('SlowInstanceCommand')) {
|
||||
return '(instance slow)';
|
||||
}
|
||||
|
||||
return '(workspace)';
|
||||
};
|
||||
|
||||
const stripCommandSuffix = (className: string): string =>
|
||||
className
|
||||
.replace(/FastInstanceCommand$/, '')
|
||||
.replace(/SlowInstanceCommand$/, '')
|
||||
.replace(/Command$/, '');
|
||||
|
||||
export const formatUpgradeCommandName = (commandName: string): string => {
|
||||
const commandNameParts = commandName.split('_');
|
||||
|
||||
if (commandNameParts.length < 3) {
|
||||
return commandName;
|
||||
}
|
||||
|
||||
const version = commandNameParts[0];
|
||||
const lastPart = commandNameParts[commandNameParts.length - 1];
|
||||
const hasTrailingTimestamp = TRAILING_TIMESTAMP_PATTERN.test(lastPart);
|
||||
|
||||
const className = commandNameParts.slice(1, -1).join('_');
|
||||
const friendlyCommandName = stripCommandSuffix(className);
|
||||
const kindLabel = getCommandKindLabel(className);
|
||||
|
||||
if (hasTrailingTimestamp) {
|
||||
return `${friendlyCommandName} ${lastPart} (${version}) ${kindLabel}`;
|
||||
}
|
||||
|
||||
return `${friendlyCommandName} (${version}) ${kindLabel}`;
|
||||
};
|
||||
Reference in New Issue
Block a user