Add workspace DDL lock env var and maintenance mode UI (#19130)
## Summary - Add `WORKSPACE_SCHEMA_DDL_LOCKED` env-only boolean config variable that blocks all workspace schema DDL changes when set to `true`. This is intended for hot upgrades where logical replication cannot handle DDL changes. Enforced at two chokepoints: - `WorkspaceMigrationRunnerService.run` — blocks all metadata-driven DDL (object/field/index CRUD, app sync/uninstall, standard app sync, upgrade commands) - `WorkspaceDataSourceService.createWorkspaceDBSchema` / `deleteWorkspaceDBSchema` — blocks workspace creation (sign-up) and hard deletion. Uses a dedicated `WorkspaceDataSourceException` (not ForbiddenException) - Add maintenance mode feature with Admin Panel UI and user-facing banner: - **Backend**: `MaintenanceModeService` stores maintenance window (startAt, endAt, optional link) in `core.keyValuePair` as `CONFIG_VARIABLE`. Validates endAt > startAt. Uses `GraphQLISODateTime` scalar for date fields. Exposed via `clientConfig` REST endpoint and admin GraphQL mutations (`setMaintenanceMode`, `clearMaintenanceMode`) - **Admin Panel**: New "Maintenance Mode" section in Health tab with UTC datetime pickers and activate/deactivate controls - **Banner**: `InformationBannerMaintenance` displayed at the top of `DefaultLayout` for all users, using Temporal API for timezone-aware formatting with an optional "Learn more" link These two features are **independent** — the DDL lock is controlled via env var for operational use, while maintenance mode is a UI notification mechanism controlled from the admin panel.
This commit is contained in:
@@ -904,12 +904,21 @@ export type ClientConfig = {
|
||||
isMicrosoftCalendarEnabled: Scalars['Boolean'];
|
||||
isMicrosoftMessagingEnabled: Scalars['Boolean'];
|
||||
isMultiWorkspaceEnabled: Scalars['Boolean'];
|
||||
isWorkspaceSchemaDDLLocked: Scalars['Boolean'];
|
||||
maintenance?: Maybe<ClientConfigMaintenanceMode>;
|
||||
publicFeatureFlags: Array<PublicFeatureFlag>;
|
||||
sentry: Sentry;
|
||||
signInPrefilled: Scalars['Boolean'];
|
||||
support: Support;
|
||||
};
|
||||
|
||||
export type ClientConfigMaintenanceMode = {
|
||||
__typename?: 'ClientConfigMaintenanceMode';
|
||||
endAt: Scalars['DateTime'];
|
||||
link?: Maybe<Scalars['String']>;
|
||||
startAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export type CollectionHash = {
|
||||
__typename?: 'CollectionHash';
|
||||
collectionName: AllMetadataName;
|
||||
@@ -2289,6 +2298,13 @@ export type LoginToken = {
|
||||
loginToken: AuthToken;
|
||||
};
|
||||
|
||||
export type MaintenanceMode = {
|
||||
__typename?: 'MaintenanceMode';
|
||||
endAt: Scalars['DateTime'];
|
||||
link?: Maybe<Scalars['String']>;
|
||||
startAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export type MarketplaceApp = {
|
||||
__typename?: 'MarketplaceApp';
|
||||
author: Scalars['String'];
|
||||
@@ -2496,6 +2512,7 @@ export type Mutation = {
|
||||
checkCustomDomainValidRecords?: Maybe<DomainValidRecords>;
|
||||
checkPublicDomainValidRecords?: Maybe<DomainValidRecords>;
|
||||
checkoutSession: BillingSession;
|
||||
clearMaintenanceMode: Scalars['Boolean'];
|
||||
createApiKey: ApiKey;
|
||||
createApplicationRegistration: CreateApplicationRegistration;
|
||||
createApplicationRegistrationVariable: ApplicationRegistrationVariable;
|
||||
@@ -2618,6 +2635,7 @@ export type Mutation = {
|
||||
setAdminAiModelRecommended: Scalars['Boolean'];
|
||||
setAdminDefaultAiModel: Scalars['Boolean'];
|
||||
setEnterpriseKey: EnterpriseLicenseInfoDto;
|
||||
setMaintenanceMode: Scalars['Boolean'];
|
||||
setMeteredSubscriptionPrice: BillingUpdate;
|
||||
signIn: AvailableWorkspacesAndAccessTokens;
|
||||
signUp: AvailableWorkspacesAndAccessTokens;
|
||||
@@ -3358,6 +3376,13 @@ export type MutationSetEnterpriseKeyArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationSetMaintenanceModeArgs = {
|
||||
endAt: Scalars['DateTime'];
|
||||
link?: InputMaybe<Scalars['String']>;
|
||||
startAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationSetMeteredSubscriptionPriceArgs = {
|
||||
priceId: Scalars['String'];
|
||||
};
|
||||
@@ -4246,6 +4271,7 @@ export type Query = {
|
||||
getEmailingDomains: Array<EmailingDomain>;
|
||||
getIndicatorHealthStatus: AdminPanelHealthServiceData;
|
||||
getLogicFunctionSourceCode?: Maybe<Scalars['String']>;
|
||||
getMaintenanceMode?: Maybe<MaintenanceMode>;
|
||||
getMeteredProductsUsage: Array<BillingMeteredProductUsage>;
|
||||
getModelsDevProviders: Array<ModelsDevProviderSuggestion>;
|
||||
getModelsDevSuggestions: Array<ModelsDevModelSuggestion>;
|
||||
@@ -7215,6 +7241,25 @@ export type GetSystemHealthStatusQueryVariables = Exact<{ [key: string]: never;
|
||||
|
||||
export type GetSystemHealthStatusQuery = { __typename?: 'Query', getSystemHealthStatus: { __typename?: 'SystemHealth', services: Array<{ __typename?: 'SystemHealthService', id: HealthIndicatorId, label: string, status: AdminPanelHealthServiceStatus }> } };
|
||||
|
||||
export type ClearMaintenanceModeMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ClearMaintenanceModeMutation = { __typename?: 'Mutation', clearMaintenanceMode: boolean };
|
||||
|
||||
export type SetMaintenanceModeMutationVariables = Exact<{
|
||||
startAt: Scalars['DateTime'];
|
||||
endAt: Scalars['DateTime'];
|
||||
link?: InputMaybe<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type SetMaintenanceModeMutation = { __typename?: 'Mutation', setMaintenanceMode: boolean };
|
||||
|
||||
export type GetMaintenanceModeQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetMaintenanceModeQuery = { __typename?: 'Query', getMaintenanceMode?: { __typename?: 'MaintenanceMode', startAt: string, endAt: string, link?: string | null } | null };
|
||||
|
||||
export type ApplicationRegistrationFragmentFragment = { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: ApplicationRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, isListed: boolean, isFeatured: boolean, ownerWorkspaceId?: string | null, createdAt: string, updatedAt: string };
|
||||
|
||||
export type DeleteApplicationRegistrationMutationVariables = Exact<{
|
||||
@@ -8330,6 +8375,9 @@ export const GetIndicatorHealthStatusDocument = {"kind":"Document","definitions"
|
||||
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>;
|
||||
export const ClearMaintenanceModeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ClearMaintenanceMode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"clearMaintenanceMode"}}]}}]} as unknown as DocumentNode<ClearMaintenanceModeMutation, ClearMaintenanceModeMutationVariables>;
|
||||
export const SetMaintenanceModeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetMaintenanceMode"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"startAt"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DateTime"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"endAt"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DateTime"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"link"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setMaintenanceMode"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"startAt"},"value":{"kind":"Variable","name":{"kind":"Name","value":"startAt"}}},{"kind":"Argument","name":{"kind":"Name","value":"endAt"},"value":{"kind":"Variable","name":{"kind":"Name","value":"endAt"}}},{"kind":"Argument","name":{"kind":"Name","value":"link"},"value":{"kind":"Variable","name":{"kind":"Name","value":"link"}}}]}]}}]} as unknown as DocumentNode<SetMaintenanceModeMutation, SetMaintenanceModeMutationVariables>;
|
||||
export const GetMaintenanceModeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetMaintenanceMode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getMaintenanceMode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"startAt"}},{"kind":"Field","name":{"kind":"Name","value":"endAt"}},{"kind":"Field","name":{"kind":"Name","value":"link"}}]}}]}}]} as unknown as DocumentNode<GetMaintenanceModeQuery, GetMaintenanceModeQueryVariables>;
|
||||
export const DeleteApplicationRegistrationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteApplicationRegistration"},"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":"deleteApplicationRegistration"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode<DeleteApplicationRegistrationMutation, DeleteApplicationRegistrationMutationVariables>;
|
||||
export const RotateApplicationRegistrationClientSecretDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RotateApplicationRegistrationClientSecret"},"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":"rotateApplicationRegistrationClientSecret"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"clientSecret"}}]}}]}}]} as unknown as DocumentNode<RotateApplicationRegistrationClientSecretMutation, RotateApplicationRegistrationClientSecretMutationVariables>;
|
||||
export const TransferApplicationRegistrationOwnershipDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TransferApplicationRegistrationOwnership"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"targetWorkspaceSubdomain"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"transferApplicationRegistrationOwnership"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"targetWorkspaceSubdomain"},"value":{"kind":"Variable","name":{"kind":"Name","value":"targetWorkspaceSubdomain"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<TransferApplicationRegistrationOwnershipMutation, TransferApplicationRegistrationOwnershipMutationVariables>;
|
||||
|
||||
Reference in New Issue
Block a user