From 468f5bbee3a3756d2d372d2ae55ced0c79eb407e Mon Sep 17 00:00:00 2001 From: Jethro Mak <88681329+Jethro-M@users.noreply.github.com> Date: Wed, 12 Jun 2024 17:26:23 -0400 Subject: [PATCH] [PLAT-13987] Add ability to skip bootstrapping Summary: **Context** We would like to give some advanced users with special use cases the ability to skip the bootstrapping (backup & restore) step when they have a legitimate reason to do so. c1fd0429ccc6e4fb59a18dd3902584fdaf3a4451 added a new boolean field in the Bootstrap params called `allowBootstrap` which simplifies the request body for clients. Instead of requiring the user to specify exactly which tables they wish to allow bootstrapping for, the YBA backend's new `allowBootstrap` field allows us to simply allow bootstrapping for all tables and databases in the universe. On the UI and docs we already inform the user that bootstrapping (aka creating a full copy) is done at the database level for YSQL. Since the user is already aware and continued to submit the form anyways, we can just convey that intention simply by passing `allowBootstrap` = true to the backend. **Changes** - Add a new runtime config flag called `yb.ui.xcluster.enable_skip_bootstrapping`. When set to true, the user will see a secondary button appear at the bottom of the create xCluster modal as well as the bottom of the select tables modal (when used for xCluster replication and not xCluster DR). This 'Skip Creating Full Copy' button will allow the user to submit their request without specifying backup storage config parameters. This will cause the YBA backend to skip the backup & restore step. - Instead of just passing tables in the `bootstrapParams`, the YBA UI will now always pass `allowBootstrapping` = true. We already inform the user that we will bootstrap the entire database for YSQL and the user is okay with this. Specifying the whole tables list again under `bootstrapParams` doesn't need to be done anymore now that the backend lets us specify that the user is okay with bootstrapping whatever is needed. We do continue passing a non-empty tables list because `tables` is still a required field. - Minor styling changes applied to the shared YBModal component. The `footerAccessory` prop will now expand to fit the remaining space in the footer. Test Plan: - Create two universes. On the desired source universe, add the following: - At least one database with tables containing no data - At least one database with tables containing data Create the same databases and tables on the target universe. - Create xCluster config Verfiy that the user is able to skip bootstrapping when `yb.ui.xcluster.enable_skip_bootstrapping` is true. {F255505} {F255506} Verify that the 'skip creating full copy' button is not shown for DR. {F255507} Verify that the 'skip creating full copy' button is not shown when `yb.ui.xcluster.enable_skip_bootstrapping` is false. {F255508} - Check the form handles back and forwards navigation after the skip bootstrapping button is pressed. (ex. verify that the back button on the next page doesn't bring the user to the configure bootstrap page) Reviewers: cwang, hzare, vbansal, rmadhavan Reviewed By: hzare Subscribers: yugaware Differential Revision: https://phorge.dev.yugabyte.com/D35768 --- managed/RUNTIME-FLAGS.md | 1 + .../yw/common/config/CustomerConfKeys.java | 11 ++ managed/src/main/resources/reference.conf | 1 + managed/ui/src/actions/xClusterReplication.ts | 59 ++++---- .../createConfig/CreateConfigModal.tsx | 137 +++++++++++++----- .../createConfig/SelectTargetUniverseStep.tsx | 3 +- .../createConfig/CreateConfigModal.tsx | 2 - .../editTables/EditTablesModal.tsx | 119 ++++++++++----- .../tableSelect/TableSelect.tsx | 19 +-- .../redesign/components/YBModal/YBModal.tsx | 12 +- managed/ui/src/redesign/helpers/constants.ts | 1 + managed/ui/src/translations/en.json | 19 ++- 12 files changed, 249 insertions(+), 135 deletions(-) diff --git a/managed/RUNTIME-FLAGS.md b/managed/RUNTIME-FLAGS.md index 443e3905fdf9..9b47f257233a 100644 --- a/managed/RUNTIME-FLAGS.md +++ b/managed/RUNTIME-FLAGS.md @@ -11,6 +11,7 @@ | "Use Redesigned Provider UI" | "yb.ui.feature_flags.provider_redesign" | "CUSTOMER" | "The redesigned provider UI adds a provider list view, a provider details view and improves the provider creation form for AWS, AZU, GCP, and K8s" | "Boolean" | | "Enable partial editing of in use providers" | "yb.ui.feature_flags.edit_in_use_provider" | "CUSTOMER" | "A subset of fields from in use providers can be edited. Users can edit in use providers directly through the YBA API. This config is used to enable this functionality through YBA UI as well." | "Boolean" | | "Show underlying xCluster configs from DR setup" | "yb.ui.xcluster.dr.show_xcluster_config" | "CUSTOMER" | "YBA creates an underlying transactional xCluster config when setting up an active-active single-master disaster recovery (DR) config. During regular operation you should manage the DR config through the DR UI instead of the xCluster UI. This feature flag serves as a way to expose the underlying xCluster config for troubleshooting." | "Boolean" | +| "Enable the option to skip creating a full copy for xCluster operations" | "yb.ui.xcluster.enable_skip_bootstrapping" | "CUSTOMER" | "Enabling this runtime config will expose an option in the create xCluster modal and select tables modal to skip creating a full copy for xCluster replication configs." | "Boolean" | | "Enforce User Tags" | "yb.universe.user_tags.is_enforced" | "CUSTOMER" | "Prevents universe creation when the enforced tags are not provided." | "Boolean" | | "Enforced User Tags List" | "yb.universe.user_tags.enforced_tags" | "CUSTOMER" | "A list of enforced user tag and accepted value pairs during universe creation. Pass '*' to accept all values for a tag. Ex: [\"yb_task:dev\",\"yb_task:test\",\"yb_owner:*\",\"yb_dept:eng\",\"yb_dept:qa\", \"yb_dept:product\", \"yb_dept:sales\"]" | "Key Value SetMultimap" | | "Enable IMDSv2" | "yb.aws.enable_imdsv2_support" | "CUSTOMER" | "Enable IMDSv2 support for AWS providers" | "Boolean" | diff --git a/managed/src/main/java/com/yugabyte/yw/common/config/CustomerConfKeys.java b/managed/src/main/java/com/yugabyte/yw/common/config/CustomerConfKeys.java index 527ca26a5e53..cce6bf50d10a 100644 --- a/managed/src/main/java/com/yugabyte/yw/common/config/CustomerConfKeys.java +++ b/managed/src/main/java/com/yugabyte/yw/common/config/CustomerConfKeys.java @@ -143,6 +143,17 @@ public class CustomerConfKeys extends RuntimeConfigKeysModule { ConfDataType.BooleanType, ImmutableList.of(ConfKeyTags.PUBLIC)); + public static final ConfKeyInfo enableSkipBootstrapping = + new ConfKeyInfo<>( + "yb.ui.xcluster.enable_skip_bootstrapping", + ScopeType.CUSTOMER, + "Enable the option to skip creating a full copy for xCluster operations", + "Enabling this runtime config will expose an option in the create xCluster modal and" + + " select tables modal to skip creating a full copy for xCluster replication" + + " configs.", + ConfDataType.BooleanType, + ImmutableList.of(ConfKeyTags.PUBLIC)); + public static final ConfKeyInfo enforceUserTags = new ConfKeyInfo<>( "yb.universe.user_tags.is_enforced", diff --git a/managed/src/main/resources/reference.conf b/managed/src/main/resources/reference.conf index 524748fd6b44..e19106cab4d5 100644 --- a/managed/src/main/resources/reference.conf +++ b/managed/src/main/resources/reference.conf @@ -181,6 +181,7 @@ yb { } xcluster { + enable_skip_bootstrapping=false dr: { # Show underlying xCluster configs used in DR on the xCluster config tab # Relevant xCluster operations can and should be done through the DR UI instead. diff --git a/managed/ui/src/actions/xClusterReplication.ts b/managed/ui/src/actions/xClusterReplication.ts index c8f23e471808..8a5d936eb573 100644 --- a/managed/ui/src/actions/xClusterReplication.ts +++ b/managed/ui/src/actions/xClusterReplication.ts @@ -37,28 +37,42 @@ export function fetchTablesInUniverse( } return Promise.reject('Querying universe tables failed: No universe UUID provided.'); } +export interface CreateXClusterConfigRequest { + name: string; + sourceUniverseUUID: string; + targetUniverseUUID: string; + configType: XClusterConfigType; + tables: string[]; -export function createXClusterReplication( - targetUniverseUUID: string, - sourceUniverseUUID: string, - name: string, - configType: XClusterConfigType, - tables: string[], - bootstrapParams: { + bootstrapParams?: { tables: string[]; - backupRequestParams: any; - } | null -) { + allowBootstrapping: boolean; + backupRequestParams: { + storageConfigUUID: string; + }; + }; +} + +export interface EditXClusterConfigTablesRequest { + tables: string[]; + + autoIncludeIndexTables?: boolean; + bootstrapParams?: { + tables: string[]; + allowBootstrapping: boolean; + backupRequestParams: { + storageConfigUUID: string; + }; + }; +} + +export function createXClusterConfig(createxClusterConfigRequest: CreateXClusterConfigRequest) { const customerId = localStorage.getItem('customerId'); return axios - .post(`${ROOT_URL}/customers/${customerId}/xcluster_configs`, { - sourceUniverseUUID, - targetUniverseUUID, - name, - configType, - tables, - ...(bootstrapParams && { bootstrapParams }) - }) + .post( + `${ROOT_URL}/customers/${customerId}/xcluster_configs`, + createxClusterConfigRequest + ) .then((response) => response.data); } @@ -135,15 +149,6 @@ export function editXclusterName(replication: XClusterConfig) { }); } -interface EditXClusterConfigTablesRequest { - tables: string[]; - autoIncludeIndexTables?: boolean; - bootstrapParams?: { - tables: string[]; - backupRequestParams: any; - }; -} - export function editXClusterConfigTables( xClusterUUID: string, { tables, autoIncludeIndexTables, bootstrapParams }: EditXClusterConfigTablesRequest diff --git a/managed/ui/src/components/xcluster/createConfig/CreateConfigModal.tsx b/managed/ui/src/components/xcluster/createConfig/CreateConfigModal.tsx index a5cc2e47d0de..0cf39401043a 100644 --- a/managed/ui/src/components/xcluster/createConfig/CreateConfigModal.tsx +++ b/managed/ui/src/components/xcluster/createConfig/CreateConfigModal.tsx @@ -1,13 +1,14 @@ import { useState } from 'react'; import { AxiosError } from 'axios'; -import { Box, Typography, useTheme } from '@material-ui/core'; +import { Box, makeStyles, Typography, useTheme } from '@material-ui/core'; import { toast } from 'react-toastify'; import { useMutation, useQuery, useQueryClient } from 'react-query'; import { useTranslation } from 'react-i18next'; import { FormProvider, SubmitHandler, useForm } from 'react-hook-form'; import { - createXClusterReplication, + createXClusterConfig, + CreateXClusterConfigRequest, fetchTablesInUniverse, fetchTaskUntilItCompletes, fetchUniverseDiskUsageMetric @@ -19,7 +20,12 @@ import { parseFloatIfDefined } from '../ReplicationUtils'; import { assertUnreachableCase, handleServerError } from '../../../utils/errorHandlingUtils'; -import { api, drConfigQueryKey, universeQueryKey } from '../../../redesign/helpers/api'; +import { + api, + drConfigQueryKey, + runtimeConfigQueryKey, + universeQueryKey +} from '../../../redesign/helpers/api'; import { YBButton, YBModal, YBModalProps } from '../../../redesign/components'; import { StorageConfigOption } from '../sharedComponents/ReactSelectStorageConfig'; import { CurrentFormStep } from './CurrentFormStep'; @@ -29,6 +35,7 @@ import { XClusterConfigType, XCLUSTER_UNIVERSE_TABLE_FILTERS } from '../constants'; +import { RuntimeConfigKey } from '../../../redesign/helpers/constants'; import { XClusterTableType } from '../XClusterTypes'; import { TableType, TableTypeLabel, Universe, YBTable } from '../../../redesign/helpers/dtos'; @@ -72,6 +79,12 @@ export const FormStep = { } as const; export type FormStep = typeof FormStep[keyof typeof FormStep]; +const useStyles = makeStyles(() => ({ + secondarySubmitButton: { + marginLeft: 'auto' + } +})); + const MODAL_NAME = 'CreateConfigModal'; const FIRST_FORM_STEP = FormStep.SELECT_TARGET_UNIVERSE; const TRANSLATION_KEY_PREFIX = 'clusterDetail.xCluster.createConfigModal'; @@ -89,7 +102,7 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf } | null>(null); const [bootstrapRequiredTableUUIDs, setBootstrapRequiredTableUUIDs] = useState([]); const [isTableSelectionValidated, setIsTableSelectionValidated] = useState(false); - + const [skipBootstrapping, setSkipBootStrapping] = useState(false); // The purpose of committedTargetUniverse is to store the targetUniverse field value prior // to the user submitting their target universe step. // This value updates whenever the user submits SelectTargetUniverseStep with a new @@ -100,24 +113,32 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf const { t } = useTranslation('translation', { keyPrefix: TRANSLATION_KEY_PREFIX }); const queryClient = useQueryClient(); const theme = useTheme(); + const classes = useStyles(); const xClusterConfigMutation = useMutation( (formValues: CreateXClusterConfigFormValues) => { - return createXClusterReplication( - formValues.targetUniverse.value.universeUUID, - sourceUniverseUuid, - formValues.configName, - formValues.isTransactionalConfig ? XClusterConfigType.TXN : XClusterConfigType.BASIC, - formValues.tableUuids.map(formatUuidForXCluster), - bootstrapRequiredTableUUIDs.length > 0 - ? { + const createXClusterConfigRequest: CreateXClusterConfigRequest = { + name: formValues.configName, + sourceUniverseUUID: sourceUniverseUuid, + targetUniverseUUID: formValues.targetUniverse.value.universeUUID, + configType: formValues.isTransactionalConfig + ? XClusterConfigType.TXN + : XClusterConfigType.BASIC, + tables: formValues.tableUuids.map(formatUuidForXCluster), + + ...(!skipBootstrapping && + bootstrapRequiredTableUUIDs.length > 0 && { + bootstrapParams: { tables: bootstrapRequiredTableUUIDs, + allowBootstrapping: true, + backupRequestParams: { storageConfigUUID: formValues.storageConfig.value.uuid } } - : null - ); + }) + }; + return createXClusterConfig(createXClusterConfigRequest); }, { onSuccess: async (response, values) => { @@ -182,6 +203,11 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf api.fetchUniverse(sourceUniverseUuid) ); + const customerUuid = localStorage.getItem('customerId') ?? ''; + const runtimeConfigQuery = useQuery(runtimeConfigQueryKey.customerScope(customerUuid), () => + api.fetchRuntimeConfigs(sourceUniverseUuid, true) + ); + const formMethods = useForm({ defaultValues: { namespaceUuids: [], @@ -195,19 +221,18 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf }); const modalTitle = t('title'); - const cancelLabel = t('cancel', { keyPrefix: 'common' }); if ( sourceUniverseTablesQuery.isLoading || sourceUniverseTablesQuery.isIdle || sourceUniverseQuery.isLoading || - sourceUniverseQuery.isIdle + sourceUniverseQuery.isIdle || + runtimeConfigQuery.isLoading || + runtimeConfigQuery.isIdle ) { return ( - + ); } @@ -278,7 +306,11 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf }; const sourceUniverseTables = sourceUniverseTablesQuery.data; - const onSubmit: SubmitHandler = async (formValues) => { + + const onSubmit = async ( + formValues: CreateXClusterConfigFormValues, + skipBootstrapping: boolean + ) => { // When the user changes target universe or table type, the old table selection is no longer valid. const isTableSelectionInvalidated = formValues.targetUniverse.value.universeUUID !== committedTargetUniverseUuid || @@ -319,6 +351,12 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf return; } + setSkipBootStrapping(skipBootstrapping); + if (skipBootstrapping) { + setCurrentFormStep(FormStep.CONFIRM_ALERT); + return; + } + if (!isTableSelectionValidated) { let bootstrapTableUuids: string[] | null = null; const hasSelectionError = false; @@ -411,7 +449,11 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf return assertUnreachableCase(currentFormStep); } }; - + const onFormSubmit: SubmitHandler = async (formValues) => + onSubmit(formValues, false); + const onSkipBootstrapAndSubmit: SubmitHandler = async ( + formValues + ) => onSubmit(formValues, true); const handleBackNavigation = () => { // We can clear errors here because prior steps have already been validated // and future steps will be revalidated when the user clicks the next page button. @@ -427,7 +469,7 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf setCurrentFormStep(FormStep.SELECT_TABLES); return; case FormStep.CONFIRM_ALERT: - if (bootstrapRequiredTableUUIDs.length > 0) { + if (bootstrapRequiredTableUUIDs.length > 0 && !skipBootstrapping) { setCurrentFormStep(FormStep.CONFIGURE_BOOTSTRAP); } else { setCurrentFormStep(FormStep.SELECT_TABLES); @@ -481,16 +523,20 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf const isTransactionalConfig = formMethods.watch('isTransactionalConfig'); const isFormDisabled = formMethods.formState.isSubmitting; + const runtimeConfigEntries = runtimeConfigQuery.data.configEntries ?? []; + const isSkipBootstrappingEnabled = runtimeConfigEntries.some( + (config: any) => + config.key === RuntimeConfigKey.ENABLE_XCLUSTER_SKIP_BOOTSTRAPPING && config.value === 'true' + ); return ( - {t('back', { keyPrefix: 'common' })} - - ) + <> + {currentFormStep !== FIRST_FORM_STEP && ( + + {t('back', { keyPrefix: 'common' })} + + )} + {currentFormStep === FormStep.SELECT_TABLES && + isBootstrapStepRequired && + isSkipBootstrappingEnabled && ( + + {t('step.selectTables.submitButton.skipBootstrapping')} + + )} + } {...modalProps} > diff --git a/managed/ui/src/components/xcluster/createConfig/SelectTargetUniverseStep.tsx b/managed/ui/src/components/xcluster/createConfig/SelectTargetUniverseStep.tsx index ae8d982469c9..fa1d21953737 100644 --- a/managed/ui/src/components/xcluster/createConfig/SelectTargetUniverseStep.tsx +++ b/managed/ui/src/components/xcluster/createConfig/SelectTargetUniverseStep.tsx @@ -278,7 +278,8 @@ export const SelectTargetUniverseStep = ({ - {!isTransactionalConfig && ( + {/* Txn xCluster is only available for YSQL. We don't warn for YCQL as we don't offer an alternative. */} + {!isTransactionalConfig && tableType === TableType.PGSQL_TABLE_TYPE && ( {t('warning.nonTxnXCluster')} diff --git a/managed/ui/src/components/xcluster/disasterRecovery/createConfig/CreateConfigModal.tsx b/managed/ui/src/components/xcluster/disasterRecovery/createConfig/CreateConfigModal.tsx index 7f434fe28a76..3778731667ff 100644 --- a/managed/ui/src/components/xcluster/disasterRecovery/createConfig/CreateConfigModal.tsx +++ b/managed/ui/src/components/xcluster/disasterRecovery/createConfig/CreateConfigModal.tsx @@ -334,11 +334,9 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf ({ + secondarySubmitButton: { + marginLeft: 'auto' + } +})); + const MODAL_NAME = 'EditTablesModal'; const TRANSLATION_KEY_PREFIX = 'clusterDetail.disasterRecovery.config.editTablesModal'; const TRANSLATION_KEY_PREFIX_SELECT_TABLE = 'clusterDetail.xCluster.selectTable'; @@ -85,7 +93,9 @@ export const EditTablesModal = (props: EditTablesModalProps) => { } | null>(null); const [bootstrapRequiredTableUUIDs, setBootstrapRequiredTableUUIDs] = useState([]); const [isTableSelectionValidated, setIsTableSelectionValidated] = useState(false); + const [skipBootstrapping, setSkipBootStrapping] = useState(false); + const classes = useStyles(); const theme = useTheme(); const queryClient = useQueryClient(); const { t } = useTranslation('translation', { keyPrefix: TRANSLATION_KEY_PREFIX }); @@ -110,17 +120,13 @@ export const EditTablesModal = (props: EditTablesModalProps) => { universeQueryKey.namespaces(xClusterConfig.sourceUniverseUUID), () => api.fetchUniverseNamespaces(xClusterConfig.sourceUniverseUUID) ); + const customerUuid = localStorage.getItem('customerId') ?? ''; + const runtimeConfigQuery = useQuery(runtimeConfigQueryKey.customerScope(customerUuid), () => + api.fetchRuntimeConfigs(customerUuid, true) + ); + const editTableMutation = useMutation( (formValues: EditTablesFormValues) => { - const bootstrapParams = - bootstrapRequiredTableUUIDs.length > 0 - ? { - tables: bootstrapRequiredTableUUIDs, - backupRequestParams: { - storageConfigUUID: formValues.storageConfig?.value.uuid - } - } - : undefined; return props.isDrInterface ? api.updateTablesInDr(props.drConfigUuid, { tables: formValues.tableUuids @@ -128,7 +134,16 @@ export const EditTablesModal = (props: EditTablesModalProps) => { : editXClusterConfigTables(xClusterConfig.uuid, { tables: formValues.tableUuids, autoIncludeIndexTables: shouldAutoIncludeIndexTables(xClusterConfig), - bootstrapParams: bootstrapParams + ...(!skipBootstrapping && + bootstrapRequiredTableUUIDs.length > 0 && { + bootstrapParams: { + tables: bootstrapRequiredTableUUIDs, + allowBootstrapping: true, + backupRequestParams: { + storageConfigUUID: formValues.storageConfig.value.uuid + } + } + }) }); }, { @@ -175,23 +190,18 @@ export const EditTablesModal = (props: EditTablesModalProps) => { } ); const modalTitle = t('title'); - const cancelLabel = t('cancel', { keyPrefix: 'common' }); if ( sourceUniverseQuery.isLoading || sourceUniverseQuery.isIdle || sourceUniverseTablesQuery.isLoading || sourceUniverseTablesQuery.isIdle || sourceUniverseNamespacesQuery.isLoading || - sourceUniverseNamespacesQuery.isIdle + sourceUniverseNamespacesQuery.isIdle || + runtimeConfigQuery.isLoading || + runtimeConfigQuery.isIdle ) { return ( - + ); @@ -209,7 +219,8 @@ export const EditTablesModal = (props: EditTablesModalProps) => { sourceUniverseQuery.isError || sourceUniverseTablesQuery.isError || sourceUniverseNamespacesQuery.isError || - !xClusterConfigTableType + !xClusterConfigTableType || + runtimeConfigQuery.isError ) { const errorMessage = !xClusterConfig.sourceUniverseUUID ? t('error.undefinedSourceUniverseUuid') @@ -217,15 +228,11 @@ export const EditTablesModal = (props: EditTablesModalProps) => { ? t('error.undefinedTargetUniverseUuid') : !xClusterConfigTableType ? t('error.undefinedXClusterTableType', { keyPrefix: TRANSLATION_KEY_PREFIX_XCLUSTER }) + : runtimeConfigQuery.isError + ? t('failedToFetchCustomerRuntimeConfig', { keyPrefix: 'queryError' }) : t('error.fetchSourceUniverseDetailsFailure'); return ( - + ); @@ -281,7 +288,7 @@ export const EditTablesModal = (props: EditTablesModalProps) => { }; const sourceUniverse = sourceUniverseQuery.data; - const onSubmit: SubmitHandler = async (formValues) => { + const onSubmit = async (formValues: EditTablesFormValues, skipBootstrapping: boolean) => { switch (currentFormStep) { case FormStep.SELECT_TABLES: { setSelectionError(null); @@ -306,6 +313,11 @@ export const EditTablesModal = (props: EditTablesModalProps) => { return; } + setSkipBootStrapping(skipBootstrapping); + if (skipBootstrapping) { + return editTableMutation.mutateAsync(formValues); + } + if (!isTableSelectionValidated) { let bootstrapTableUuids: string[] | null = null; const hasSelectionError = false; @@ -399,6 +411,10 @@ export const EditTablesModal = (props: EditTablesModalProps) => { return assertUnreachableCase(currentFormStep); } }; + const onFormSubmit: SubmitHandler = async (formValues) => + onSubmit(formValues, false); + const onSkipBootstrapAndSubmit: SubmitHandler = async (formValues) => + onSubmit(formValues, true); const handleBackNavigation = () => { switch (currentFormStep) { @@ -417,9 +433,13 @@ export const EditTablesModal = (props: EditTablesModalProps) => { case FormStep.SELECT_TABLES: return isTableSelectionValidated ? bootstrapRequiredTableUUIDs.length > 0 - ? t('step.selectTables.nextButton') + ? t( + `step.selectTables.submitButton.configureBootstrap.${ + props.isDrInterface ? 'dr' : 'xCluster' + }` + ) : t('applyChanges', { keyPrefix: 'common' }) - : t('submitButton.validate'); + : t('step.selectTables.submitButton.validateSelection'); case FormStep.CONFIGURE_BOOTSTRAP: return t('applyChanges', { keyPrefix: 'common' }); default: @@ -431,25 +451,44 @@ export const EditTablesModal = (props: EditTablesModalProps) => { const selectedTableUuids = formMethods.watch('tableUuids'); const selectedNamespaceUuids = formMethods.watch('namespaceUuids'); const isFormDisabled = formMethods.formState.isSubmitting; + const runtimeConfigEntries = runtimeConfigQuery.data.configEntries ?? []; + const isSkipBootstrappingEnabled = runtimeConfigEntries.some( + (config: any) => + config.key === RuntimeConfigKey.ENABLE_XCLUSTER_SKIP_BOOTSTRAPPING && config.value === 'true' + ); return ( - {t('back', { keyPrefix: 'common' })} - - ) + <> + {currentFormStep !== FIRST_FORM_STEP && ( + + {t('back', { keyPrefix: 'common' })} + + )} + {currentFormStep === FormStep.SELECT_TABLES && + !props.isDrInterface && + isSkipBootstrappingEnabled && ( + + {t('step.selectTables.submitButton.skipBootstrapping')} + + )} + } {...modalProps} > diff --git a/managed/ui/src/components/xcluster/sharedComponents/tableSelect/TableSelect.tsx b/managed/ui/src/components/xcluster/sharedComponents/tableSelect/TableSelect.tsx index 9a7d274627bb..233fdb3b80e9 100644 --- a/managed/ui/src/components/xcluster/sharedComponents/tableSelect/TableSelect.tsx +++ b/managed/ui/src/components/xcluster/sharedComponents/tableSelect/TableSelect.tsx @@ -13,12 +13,7 @@ import { fetchTablesInUniverse, fetchXClusterConfig } from '../../../../actions/xClusterReplication'; -import { - api, - runtimeConfigQueryKey, - universeQueryKey, - xClusterQueryKey -} from '../../../../redesign/helpers/api'; +import { api, universeQueryKey, xClusterQueryKey } from '../../../../redesign/helpers/api'; import { YBControlledSelect, YBInputField } from '../../../common/forms/fields'; import { YBErrorIndicator, YBLoading } from '../../../common/indicators'; import { hasSubstringMatch } from '../../../queries/helpers/queriesHelper'; @@ -41,7 +36,6 @@ import { ExpandedTableSelect } from './ExpandedTableSelect'; import { XClusterTableEligibility } from '../../constants'; import { assertUnreachableCase } from '../../../../utils/errorHandlingUtils'; import { SortOrder, YBTableRelationType } from '../../../../redesign/helpers/constants'; -import { DEFAULT_RUNTIME_GLOBAL_SCOPE } from '../../../../actions/customers'; import { ExpandColumnComponent } from './ExpandColumnComponent'; import { getTableUuid } from '../../../../utils/tableUtils'; import { YBBanner, YBBannerVariant } from '../../../common/descriptors'; @@ -216,10 +210,6 @@ export const TableSelect = (props: TableSelectProps) => { // Upgrading react-query to v3.28 may solve this issue: https://github.com/TanStack/query/issues/1675 ) as UseQueryResult[]; - const globalRuntimeConfigQuery = useQuery(runtimeConfigQueryKey.globalScope(), () => - api.fetchRuntimeConfigs(DEFAULT_RUNTIME_GLOBAL_SCOPE, true) - ); - if ( sourceUniverseNamespaceQuery.isLoading || sourceUniverseNamespaceQuery.isIdle || @@ -230,9 +220,7 @@ export const TableSelect = (props: TableSelectProps) => { sourceUniverseQuery.isLoading || sourceUniverseQuery.isIdle || targetUniverseQuery.isLoading || - targetUniverseQuery.isIdle || - globalRuntimeConfigQuery.isLoading || - globalRuntimeConfigQuery.isIdle + targetUniverseQuery.isIdle ) { return ; } @@ -253,9 +241,6 @@ export const TableSelect = (props: TableSelectProps) => { ); } - if (globalRuntimeConfigQuery.isError) { - return ; - } const toggleTableGroup = ( isSelected: boolean, diff --git a/managed/ui/src/redesign/components/YBModal/YBModal.tsx b/managed/ui/src/redesign/components/YBModal/YBModal.tsx index 474892d484eb..d74b0a27bfce 100644 --- a/managed/ui/src/redesign/components/YBModal/YBModal.tsx +++ b/managed/ui/src/redesign/components/YBModal/YBModal.tsx @@ -45,6 +45,7 @@ export interface YBModalProps extends DialogProps { dialogContentProps?: DialogContentProps; titleContentProps?: string; isSubmitting?: boolean; + showSubmitSpinner?: boolean; } export const SlideTransition = React.forwardRef( @@ -115,7 +116,9 @@ const useStyles = makeStyles>((theme) => ({ lineHeight: '26px' }, footerAccessory: { - marginRight: 'auto' + display: 'flex', + flexShrink: 1, + width: '100%' }, title: { display: 'flex' @@ -130,6 +133,9 @@ const useStyles = makeStyles>((theme) => ({ text: { marginLeft: theme.spacing(1), whiteSpace: 'nowrap' + }, + submitButton: { + flexShrink: 0 } })); @@ -155,6 +161,7 @@ export const YBModal: FC = (props: YBModalProps) => { submitButtonTooltip, cancelButtonTooltip, isSubmitting, + showSubmitSpinner = true, dialogContentProps = { dividers: true }, titleContentProps, ...dialogProps @@ -246,10 +253,11 @@ export const YBModal: FC = (props: YBModalProps) => { {submitLabel} diff --git a/managed/ui/src/redesign/helpers/constants.ts b/managed/ui/src/redesign/helpers/constants.ts index 51fb28ebfc91..a85efc419458 100644 --- a/managed/ui/src/redesign/helpers/constants.ts +++ b/managed/ui/src/redesign/helpers/constants.ts @@ -38,6 +38,7 @@ export const RuntimeConfigKey = { PROVIDER_REDESIGN_UI_FEATURE_FLAG: 'yb.ui.feature_flags.provider_redesign', EDIT_IN_USE_PORIVDER_UI_FEATURE_FLAG: 'yb.ui.feature_flags.edit_in_use_provider', XCLUSTER_TRANSACTIONAL_ATOMICITY_FEATURE_FLAG: 'yb.xcluster.transactional.enabled', + ENABLE_XCLUSTER_SKIP_BOOTSTRAPPING: 'yb.ui.xcluster.enable_skip_bootstrapping', DISASTER_RECOVERY_FEATURE_FLAG: 'yb.xcluster.dr.enabled', PERFOMANCE_ADVISOR_UI_FEATURE_FLAG: 'yb.ui.feature_flags.perf_advisor', GRANULAR_METRICS_FEATURE_FLAG: 'yb.ui.feature_flags.granular_metrics', diff --git a/managed/ui/src/translations/en.json b/managed/ui/src/translations/en.json index 4cd46ba7e823..85ffb7b7fb3e 100644 --- a/managed/ui/src/translations/en.json +++ b/managed/ui/src/translations/en.json @@ -962,7 +962,14 @@ "dr": "Select the tables to enable disaster recovery for", "xCluster": "Select the tables to enable xCluster replication for" }, - "nextButton": "Next: Confirm Full Copy" + "submitButton": { + "skipBootstrapping": "Skip Creating Full Copy", + "validateSelection": "Validate Table Selection", + "configureBootstrap": { + "dr": "Next: Confirm Full Copy", + "xCluster": "Next: Configure Full Copy" + } + } }, "configureBootstrap": { "infoText": { @@ -977,10 +984,6 @@ } } }, - "submitButton": { - "validate": "Validate Selection", - "applyChanges": "Apply Changes" - }, "error": { "requestFailureLabel": "Edit tables request failed", "taskFailure": "Edit tables task failed.", @@ -1053,11 +1056,11 @@ "ysql": "Select the databases to enable xCluster replication for", "ycql": "Select the tables to enable xCluster replication for" }, - "validateButton": "Validate Table Selection", "submitButton": { - "validateSelection": "Validate Selection", + "validateSelection": "Validate Table Selection", "configureBootstrap": "Next: Configure Full Copy", - "confirmAlert": "Next: Confirm Alert Threshold" + "confirmAlert": "Next: Confirm Alert Threshold", + "skipBootstrapping": "Skip Creating Full Copy" } }, "configureBootstrap": {