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:
@@ -12,6 +12,8 @@ import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConf
|
||||
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
|
||||
import { isClickHouseConfiguredState } from '@/client-config/states/isClickHouseConfiguredState';
|
||||
import { isCloudflareIntegrationEnabledState } from '@/client-config/states/isCloudflareIntegrationEnabledState';
|
||||
import { isDDLLockedState } from '@/client-config/states/isDDLLockedState';
|
||||
import { maintenanceModeState } from '@/client-config/states/maintenanceModeState';
|
||||
import { isEmailingDomainsEnabledState } from '@/client-config/states/isEmailingDomainsEnabledState';
|
||||
import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState';
|
||||
import { isGoogleCalendarEnabledState } from '@/client-config/states/isGoogleCalendarEnabledState';
|
||||
@@ -117,6 +119,10 @@ export const useClientConfig = (): UseClientConfigResult => {
|
||||
isClickHouseConfiguredState,
|
||||
);
|
||||
|
||||
const setIsDDLLocked = useSetAtomState(isDDLLockedState);
|
||||
|
||||
const setMaintenanceMode = useSetAtomState(maintenanceModeState);
|
||||
|
||||
const setAppVersion = useSetAtomState(appVersionState);
|
||||
|
||||
const fetchClientConfig = useCallback(async () => {
|
||||
@@ -195,6 +201,8 @@ export const useClientConfig = (): UseClientConfigResult => {
|
||||
clientConfig?.isCloudflareIntegrationEnabled,
|
||||
);
|
||||
setIsClickHouseConfigured(clientConfig?.isClickHouseConfigured ?? false);
|
||||
setIsDDLLocked(clientConfig?.isWorkspaceSchemaDDLLocked ?? false);
|
||||
setMaintenanceMode(clientConfig?.maintenance ?? null);
|
||||
} catch (err) {
|
||||
const error =
|
||||
err instanceof Error ? err : new Error('Failed to fetch client config');
|
||||
@@ -229,7 +237,9 @@ export const useClientConfig = (): UseClientConfigResult => {
|
||||
setIsEmailingDomainsEnabled,
|
||||
setIsClickHouseConfigured,
|
||||
setIsCloudflareIntegrationEnabled,
|
||||
setIsDDLLocked,
|
||||
setLabPublicFeatureFlags,
|
||||
setMaintenanceMode,
|
||||
setIsMicrosoftCalendarEnabled,
|
||||
setIsMicrosoftMessagingEnabled,
|
||||
setSentryConfig,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const isDDLLockedState = createAtomState<boolean>({
|
||||
key: 'isDDLLocked',
|
||||
defaultValue: false,
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
import { type ClientConfigMaintenanceMode } from '~/generated-metadata/graphql';
|
||||
|
||||
type MaintenanceModeState = Pick<
|
||||
ClientConfigMaintenanceMode,
|
||||
'startAt' | 'endAt' | 'link'
|
||||
>;
|
||||
|
||||
export const maintenanceModeState =
|
||||
createAtomState<MaintenanceModeState | null>({
|
||||
key: 'maintenanceModeState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type Billing,
|
||||
type Captcha,
|
||||
type ClientAiModelConfig,
|
||||
type ClientConfigMaintenanceMode,
|
||||
type PublicFeatureFlag,
|
||||
type Sentry,
|
||||
type Support,
|
||||
@@ -33,10 +34,12 @@ export type ClientConfig = {
|
||||
isEmailingDomainsEnabled: boolean;
|
||||
isCloudflareIntegrationEnabled: boolean;
|
||||
isClickHouseConfigured: boolean;
|
||||
isWorkspaceSchemaDDLLocked: boolean;
|
||||
publicFeatureFlags: Array<PublicFeatureFlag>;
|
||||
sentry: Sentry;
|
||||
signInPrefilled: boolean;
|
||||
support: Support;
|
||||
isTwoFactorAuthenticationEnabled: boolean;
|
||||
allowRequestsToTwentyIcons: boolean;
|
||||
maintenance?: ClientConfigMaintenanceMode;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user