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:
@@ -1737,6 +1737,12 @@ enum FeatureFlagKey {
|
||||
IS_DATASOURCE_MIGRATED
|
||||
}
|
||||
|
||||
type ClientConfigMaintenanceMode {
|
||||
startAt: DateTime!
|
||||
endAt: DateTime!
|
||||
link: String
|
||||
}
|
||||
|
||||
type ClientConfig {
|
||||
appVersion: String
|
||||
authProviders: AuthProviders!
|
||||
@@ -1765,6 +1771,8 @@ type ClientConfig {
|
||||
calendarBookingPageId: String
|
||||
isCloudflareIntegrationEnabled: Boolean!
|
||||
isClickHouseConfigured: Boolean!
|
||||
isWorkspaceSchemaDDLLocked: Boolean!
|
||||
maintenance: ClientConfigMaintenanceMode
|
||||
}
|
||||
|
||||
type UsageBreakdownItem {
|
||||
@@ -1961,6 +1969,12 @@ type AdminPanelHealthServiceData {
|
||||
queues: [AdminPanelWorkerQueueHealth!]
|
||||
}
|
||||
|
||||
type MaintenanceMode {
|
||||
startAt: DateTime!
|
||||
endAt: DateTime!
|
||||
link: String
|
||||
}
|
||||
|
||||
type ModelsDevModelSuggestion {
|
||||
modelId: String!
|
||||
name: String!
|
||||
@@ -3225,6 +3239,7 @@ type Query {
|
||||
getModelsDevProviders: [ModelsDevProviderSuggestion!]!
|
||||
getModelsDevSuggestions(providerType: String!): [ModelsDevModelSuggestion!]!
|
||||
getAdminAiUsageByWorkspace(periodStart: DateTime, periodEnd: DateTime): [UsageBreakdownItem!]!
|
||||
getMaintenanceMode: MaintenanceMode
|
||||
getUsageAnalytics(input: UsageAnalyticsInput): UsageAnalytics!
|
||||
getPostgresCredentials: PostgresCredentials
|
||||
findManyPublicDomains: [PublicDomain!]!
|
||||
@@ -3545,6 +3560,8 @@ type Mutation {
|
||||
removeAiProvider(providerName: String!): Boolean!
|
||||
addModelToProvider(providerName: String!, modelConfig: JSON!): Boolean!
|
||||
removeModelFromProvider(providerName: String!, modelName: String!): Boolean!
|
||||
setMaintenanceMode(startAt: DateTime!, endAt: DateTime!, link: String): Boolean!
|
||||
clearMaintenanceMode: Boolean!
|
||||
enablePostgresProxy: PostgresCredentials!
|
||||
disablePostgresProxy: PostgresCredentials!
|
||||
createPublicDomain(domain: String!): PublicDomain!
|
||||
|
||||
@@ -1424,6 +1424,13 @@ export interface PublicFeatureFlag {
|
||||
|
||||
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_USAGE_ANALYTICS_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' | 'IS_GRAPHQL_QUERY_TIMING_ENABLED' | 'IS_RECORD_TABLE_WIDGET_ENABLED' | 'IS_DATASOURCE_MIGRATED'
|
||||
|
||||
export interface ClientConfigMaintenanceMode {
|
||||
startAt: Scalars['DateTime']
|
||||
endAt: Scalars['DateTime']
|
||||
link?: Scalars['String']
|
||||
__typename: 'ClientConfigMaintenanceMode'
|
||||
}
|
||||
|
||||
export interface ClientConfig {
|
||||
appVersion?: Scalars['String']
|
||||
authProviders: AuthProviders
|
||||
@@ -1452,6 +1459,8 @@ export interface ClientConfig {
|
||||
calendarBookingPageId?: Scalars['String']
|
||||
isCloudflareIntegrationEnabled: Scalars['Boolean']
|
||||
isClickHouseConfigured: Scalars['Boolean']
|
||||
isWorkspaceSchemaDDLLocked: Scalars['Boolean']
|
||||
maintenance?: ClientConfigMaintenanceMode
|
||||
__typename: 'ClientConfig'
|
||||
}
|
||||
|
||||
@@ -1621,6 +1630,13 @@ export interface AdminPanelHealthServiceData {
|
||||
__typename: 'AdminPanelHealthServiceData'
|
||||
}
|
||||
|
||||
export interface MaintenanceMode {
|
||||
startAt: Scalars['DateTime']
|
||||
endAt: Scalars['DateTime']
|
||||
link?: Scalars['String']
|
||||
__typename: 'MaintenanceMode'
|
||||
}
|
||||
|
||||
export interface ModelsDevModelSuggestion {
|
||||
modelId: Scalars['String']
|
||||
name: Scalars['String']
|
||||
@@ -2776,6 +2792,7 @@ export interface Query {
|
||||
getModelsDevProviders: ModelsDevProviderSuggestion[]
|
||||
getModelsDevSuggestions: ModelsDevModelSuggestion[]
|
||||
getAdminAiUsageByWorkspace: UsageBreakdownItem[]
|
||||
getMaintenanceMode?: MaintenanceMode
|
||||
getUsageAnalytics: UsageAnalytics
|
||||
getPostgresCredentials?: PostgresCredentials
|
||||
findManyPublicDomains: PublicDomain[]
|
||||
@@ -2991,6 +3008,8 @@ export interface Mutation {
|
||||
removeAiProvider: Scalars['Boolean']
|
||||
addModelToProvider: Scalars['Boolean']
|
||||
removeModelFromProvider: Scalars['Boolean']
|
||||
setMaintenanceMode: Scalars['Boolean']
|
||||
clearMaintenanceMode: Scalars['Boolean']
|
||||
enablePostgresProxy: PostgresCredentials
|
||||
disablePostgresProxy: PostgresCredentials
|
||||
createPublicDomain: PublicDomain
|
||||
@@ -4508,6 +4527,14 @@ export interface PublicFeatureFlagGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ClientConfigMaintenanceModeGenqlSelection{
|
||||
startAt?: boolean | number
|
||||
endAt?: boolean | number
|
||||
link?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ClientConfigGenqlSelection{
|
||||
appVersion?: boolean | number
|
||||
authProviders?: AuthProvidersGenqlSelection
|
||||
@@ -4536,6 +4563,8 @@ export interface ClientConfigGenqlSelection{
|
||||
calendarBookingPageId?: boolean | number
|
||||
isCloudflareIntegrationEnabled?: boolean | number
|
||||
isClickHouseConfigured?: boolean | number
|
||||
isWorkspaceSchemaDDLLocked?: boolean | number
|
||||
maintenance?: ClientConfigMaintenanceModeGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
@@ -4711,6 +4740,14 @@ export interface AdminPanelHealthServiceDataGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface MaintenanceModeGenqlSelection{
|
||||
startAt?: boolean | number
|
||||
endAt?: boolean | number
|
||||
link?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ModelsDevModelSuggestionGenqlSelection{
|
||||
modelId?: boolean | number
|
||||
name?: boolean | number
|
||||
@@ -5966,6 +6003,7 @@ export interface QueryGenqlSelection{
|
||||
getModelsDevProviders?: ModelsDevProviderSuggestionGenqlSelection
|
||||
getModelsDevSuggestions?: (ModelsDevModelSuggestionGenqlSelection & { __args: {providerType: Scalars['String']} })
|
||||
getAdminAiUsageByWorkspace?: (UsageBreakdownItemGenqlSelection & { __args?: {periodStart?: (Scalars['DateTime'] | null), periodEnd?: (Scalars['DateTime'] | null)} })
|
||||
getMaintenanceMode?: MaintenanceModeGenqlSelection
|
||||
getUsageAnalytics?: (UsageAnalyticsGenqlSelection & { __args?: {input?: (UsageAnalyticsInput | null)} })
|
||||
getPostgresCredentials?: PostgresCredentialsGenqlSelection
|
||||
findManyPublicDomains?: PublicDomainGenqlSelection
|
||||
@@ -6200,6 +6238,8 @@ export interface MutationGenqlSelection{
|
||||
removeAiProvider?: { __args: {providerName: Scalars['String']} }
|
||||
addModelToProvider?: { __args: {providerName: Scalars['String'], modelConfig: Scalars['JSON']} }
|
||||
removeModelFromProvider?: { __args: {providerName: Scalars['String'], modelName: Scalars['String']} }
|
||||
setMaintenanceMode?: { __args: {startAt: Scalars['DateTime'], endAt: Scalars['DateTime'], link?: (Scalars['String'] | null)} }
|
||||
clearMaintenanceMode?: boolean | number
|
||||
enablePostgresProxy?: PostgresCredentialsGenqlSelection
|
||||
disablePostgresProxy?: PostgresCredentialsGenqlSelection
|
||||
createPublicDomain?: (PublicDomainGenqlSelection & { __args: {domain: Scalars['String']} })
|
||||
@@ -7502,6 +7542,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const ClientConfigMaintenanceMode_possibleTypes: string[] = ['ClientConfigMaintenanceMode']
|
||||
export const isClientConfigMaintenanceMode = (obj?: { __typename?: any } | null): obj is ClientConfigMaintenanceMode => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isClientConfigMaintenanceMode"')
|
||||
return ClientConfigMaintenanceMode_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ClientConfig_possibleTypes: string[] = ['ClientConfig']
|
||||
export const isClientConfig = (obj?: { __typename?: any } | null): obj is ClientConfig => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isClientConfig"')
|
||||
@@ -7662,6 +7710,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const MaintenanceMode_possibleTypes: string[] = ['MaintenanceMode']
|
||||
export const isMaintenanceMode = (obj?: { __typename?: any } | null): obj is MaintenanceMode => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isMaintenanceMode"')
|
||||
return MaintenanceMode_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ModelsDevModelSuggestion_possibleTypes: string[] = ['ModelsDevModelSuggestion']
|
||||
export const isModelsDevModelSuggestion = (obj?: { __typename?: any } | null): obj is ModelsDevModelSuggestion => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isModelsDevModelSuggestion"')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user