diff --git a/docs/en_US/cloud_edb_biganimal.rst b/docs/en_US/cloud_edb_biganimal.rst index f1f9a8cfb..bd0e93093 100644 --- a/docs/en_US/cloud_edb_biganimal.rst +++ b/docs/en_US/cloud_edb_biganimal.rst @@ -23,6 +23,18 @@ will be redirected to the new tab for the verification. Once you confirm the one time code, the pgAdmin will automatically detect it and the next button will be enabled. To proceed further, click on the next button. +.. image:: images/cloud_biganimal_cluster.png + :alt: Cloud Deployment Provider + :align: center + + +* Use the *Cluster type* field to choose a cluster type. + +* Use the *No. of Standby Replicas* field to specify the replicas if you have selected the High Availability cluster. + +* Use the *Cluster provider* field to coose the provider. + + .. image:: images/cloud_biganimal_instance.png :alt: Cloud Deployment Provider :align: center @@ -48,8 +60,11 @@ details. * Use the *Volume type* field to select the instance storage type. -* Use the *Volume properties* field to specify the storage capacity. +* Use the *Volume properties* field to specify the storage capacity. This field is specific to Azure. +* Use the *Volume size* field to specify the storage size. This field is specific to AWS. + +* Use the *Volume IOPS* field to specify the storage IOPS. This field is specific to AWS. .. image:: images/cloud_biganimal_database.png @@ -72,11 +87,6 @@ Use the fields from the Database Details tab to specify the Instance details. * Use the *Confirm password* field to repeat the password. -* Use the *High Availability* field to create the cluster with high availability, which creates a cluster - with one primary and up to two standby replicas in different availability zones. - -* Use the *Number of standby replicas* field to specify the standby replicas. - .. image:: images/cloud_biganimal_review.png :alt: Cloud Deployment Provider diff --git a/docs/en_US/images/cloud_biganimal_cluster.png b/docs/en_US/images/cloud_biganimal_cluster.png new file mode 100644 index 000000000..f337d3248 Binary files /dev/null and b/docs/en_US/images/cloud_biganimal_cluster.png differ diff --git a/docs/en_US/images/cloud_biganimal_credentials.png b/docs/en_US/images/cloud_biganimal_credentials.png index f5334d4b5..9b9817723 100644 Binary files a/docs/en_US/images/cloud_biganimal_credentials.png and b/docs/en_US/images/cloud_biganimal_credentials.png differ diff --git a/docs/en_US/images/cloud_biganimal_database.png b/docs/en_US/images/cloud_biganimal_database.png index 1e00e52a2..68d0deee3 100644 Binary files a/docs/en_US/images/cloud_biganimal_database.png and b/docs/en_US/images/cloud_biganimal_database.png differ diff --git a/docs/en_US/images/cloud_biganimal_instance.png b/docs/en_US/images/cloud_biganimal_instance.png index d8e2183fd..d94eb40c0 100644 Binary files a/docs/en_US/images/cloud_biganimal_instance.png and b/docs/en_US/images/cloud_biganimal_instance.png differ diff --git a/docs/en_US/images/cloud_biganimal_provider.png b/docs/en_US/images/cloud_biganimal_provider.png index 27579b7e0..6ad09e8ca 100644 Binary files a/docs/en_US/images/cloud_biganimal_provider.png and b/docs/en_US/images/cloud_biganimal_provider.png differ diff --git a/docs/en_US/images/cloud_biganimal_review.png b/docs/en_US/images/cloud_biganimal_review.png index 6ccf35c6e..3b65e1e6d 100644 Binary files a/docs/en_US/images/cloud_biganimal_review.png and b/docs/en_US/images/cloud_biganimal_review.png differ diff --git a/web/pgacloud/providers/biganimal.py b/web/pgacloud/providers/biganimal.py index 85fa9f25a..dc075f17c 100644 --- a/web/pgacloud/providers/biganimal.py +++ b/web/pgacloud/providers/biganimal.py @@ -65,6 +65,12 @@ class BigAnimalProvider(AbsProvider): parser_create_instance.add_argument('--volume-properties', required=True, help='storage properties') + parser_create_instance.add_argument('--volume-size', + required=True, + help='storage Size') + parser_create_instance.add_argument('--volume-IOPS', + required=True, + help='storage IOPS') parser_create_instance.add_argument('--private-network', required=True, help='Private or Public Network') parser_create_instance.add_argument('--public-ip', default='', @@ -76,6 +82,12 @@ class BigAnimalProvider(AbsProvider): parser_create_instance.add_argument('--nodes', required=True, help='No of Nodes') + parser_create_instance.add_argument('--replicas', + required=True, + help='No. of Stand By Replicas') + parser_create_instance.add_argument('--cloud-provider', + required=True, + help='Provider') def cmd_create_instance(self, args): """ Create a biganimal cluster """ @@ -105,12 +117,14 @@ class BigAnimalProvider(AbsProvider): 'pgType': {'pgTypeId': args.db_type}, 'pgVersion': {'pgVersionId': args.db_version}, 'privateNetworking': private_network, - 'provider': {'cloudProviderId': 'azure'}, + 'provider': {'cloudProviderId': args.cloud_provider}, 'region': {'regionId': args.region}, - 'replicas': 1, + 'replicas': int(args.replicas), 'storage': { 'volumePropertiesId': args.volume_properties, - 'volumeTypeId': args.volume_type + 'volumeTypeId': args.volume_type, + 'iops': args.volume_IOPS, + 'size': args.volume_size + ' Gi' }, 'clusterArchitecture': { 'clusterArchitectureId': args.cluster_arch, diff --git a/web/pgadmin/misc/cloud/biganimal/__init__.py b/web/pgadmin/misc/cloud/biganimal/__init__.py index 55d7a491f..6e6c4f7fd 100644 --- a/web/pgadmin/misc/cloud/biganimal/__init__.py +++ b/web/pgadmin/misc/cloud/biganimal/__init__.py @@ -50,7 +50,8 @@ class BigAnimalModule(PgAdminModule): 'biganimal.db_versions', 'biganimal.instance_types', 'biganimal.volume_types', - 'biganimal.volume_properties'] + 'biganimal.volume_properties', + 'biganimal.providers'] blueprint = BigAnimalModule(MODULE_NAME, __name__, @@ -82,17 +83,27 @@ def verification(): return make_json_response(data=verification_uri) -@blueprint.route('/regions/', +@blueprint.route('/regions/', methods=['GET'], endpoint='regions') @login_required -def biganimal_regions(): +def biganimal_regions(provider_id): """Get Regions.""" biganimal_obj = pickle.loads(session['biganimal']['provider_obj']) - status, regions = biganimal_obj.get_regions() + status, regions = biganimal_obj.get_regions(provider_id) session['biganimal']['provider_obj'] = pickle.dumps(biganimal_obj, -1) return make_json_response(data=regions) +@blueprint.route('/providers/', + methods=['GET'], endpoint='providers') +@login_required +def biganimal_providers(): + """Get Providers.""" + biganimal_obj = pickle.loads(session['biganimal']['provider_obj']) + status, providers = biganimal_obj.get_providers() + return make_json_response(data=providers) + + @blueprint.route('/db_types/', methods=['GET'], endpoint='db_types') @login_required @@ -103,50 +114,52 @@ def biganimal_db_types(): return make_json_response(data=pg_types) -@blueprint.route('/db_versions/', +@blueprint.route('/db_versions//', methods=['GET'], endpoint='db_versions') @login_required -def biganimal_db_versions(db_type): +def biganimal_db_versions(cluster_type, pg_type): """Get Database Version.""" biganimal_obj = pickle.loads(session['biganimal']['provider_obj']) - pg_versions = biganimal_obj.get_postgres_versions(db_type) + pg_versions = biganimal_obj.get_postgres_versions(cluster_type, pg_type) return make_json_response(data=pg_versions) -@blueprint.route('/instance_types/', +@blueprint.route('/instance_types//', methods=['GET'], endpoint='instance_types') @login_required -def biganimal_instance_types(region_id): +def biganimal_instance_types(region_id, provider_id): """Get Instance Types.""" - if not region_id: + if not region_id or not provider_id: return make_json_response(data=[]) biganimal_obj = pickle.loads(session['biganimal']['provider_obj']) - biganimal_instances = biganimal_obj.get_instance_types(region_id) + biganimal_instances = biganimal_obj.get_instance_types(region_id, + provider_id) return make_json_response(data=biganimal_instances) -@blueprint.route('/volume_types/', +@blueprint.route('/volume_types//', methods=['GET'], endpoint='volume_types') @login_required -def biganimal_volume_types(region_id): +def biganimal_volume_types(region_id, provider_id): """Get Volume Types.""" - if not region_id: + if not region_id or not provider_id: return make_json_response(data=[]) biganimal_obj = pickle.loads(session['biganimal']['provider_obj']) - biganimal_volumes = biganimal_obj.get_volume_types(region_id) + biganimal_volumes = biganimal_obj.get_volume_types(region_id, provider_id) return make_json_response(data=biganimal_volumes) -@blueprint.route('/volume_properties//', +@blueprint.route('/volume_properties///', methods=['GET'], endpoint='volume_properties') @login_required -def biganimal_volume_properties(region_id, volume_type): +def biganimal_volume_properties(region_id, provider_id, volume_type): """Get Volume Properties.""" - if not region_id: + if not region_id or not provider_id: return make_json_response(data=[]) biganimal_obj = pickle.loads(session['biganimal']['provider_obj']) biganimal_volume_properties = biganimal_obj.get_volume_properties( region_id, + provider_id, volume_type) return make_json_response(data=biganimal_volume_properties) @@ -261,19 +274,42 @@ class BigAnimalProvider(): # so all the existing clusters moved to the default Project. # For now, we can get the Proj Id by replacing 'org' to 'prj' # in organization ID: org_1234 -> prj_1234 - proj_Id = content['data']['organizationId'].replace('org', + proj_id = content['data']['organizationId'].replace('org', 'prj') for permission in content['data']['scopedPermissions']: - if proj_Id == permission['scope'] and\ + if proj_id == permission['scope'] and\ 'create:clusters' in permission['permissions']: return True return False - def get_regions(self): + def get_providers(self): + """Get cloud providers""" + _url = '{0}/cloud-providers'.format( + self.BASE_URL) + providers = [] + resp = requests.get(_url, headers=self._get_headers()) + if resp.status_code == 200 and resp.content: + provider_resp = json.loads(resp.content) + for value in provider_resp['data']: + providers.append({ + 'label': value['cloudProviderName'], + 'value': value['cloudProviderId'], + 'connected': value['connected'] + }) + return True, providers + elif resp.content: + provider_resp = json.loads(resp.content) + return False, provider_resp['error']['message'] + else: + return False, gettext('Error retrieving providers.') + + def get_regions(self, provider_id): """Get regions""" - _url = "{0}/{1}".format( + if not provider_id: + return False, gettext('Provider not provided.') + _url = '{0}/cloud-providers/{1}/regions'.format( self.BASE_URL, - 'cloud-providers/azure/regions') + provider_id) regions = [] resp = requests.get(_url, headers=self._get_headers()) if resp.status_code == 200 and resp.content: @@ -309,12 +345,15 @@ class BigAnimalProvider(): }) return pg_types - def get_postgres_versions(self, db_type): + def get_postgres_versions(self, cluster_type, pg_type): """Get Postgres Versions.""" - _url = "{0}/pg-versions?pgTypeIds={1}".format( - self.BASE_URL, - db_type - ) + if not cluster_type or not pg_type: + return [] + + _url = "{0}/pg-versions?clusterArchitectureIds={1}" \ + "&pgTypeIds={2}".format(self.BASE_URL, + cluster_type, + pg_type) pg_versions = [] resp = requests.get(_url, headers=self._get_headers()) if resp.status_code == 200 and resp.content: @@ -326,48 +365,55 @@ class BigAnimalProvider(): }) return pg_versions - def get_instance_types(self, region_id): + def get_instance_types(self, region_id, provider_id): """GEt Instance Types.""" - if region_id not in self.regions: + if region_id not in self.regions or not provider_id: return [] - _url = "{0}/{1}".format( - self.BASE_URL, - 'cloud-providers/azure/regions/' - '{0}/instance-types'.format(region_id)) + _url = '{0}/cloud-providers/{1}/regions/{2}/instance-types?' \ + 'sort=instanceTypeName'.format(self.BASE_URL, + provider_id, + region_id) resp = requests.get(_url, headers=self._get_headers()) if resp.status_code == 200 and resp.content: pg_types = json.loads(resp.content) - return pg_types['data'] + _sorted_data = sorted(pg_types['data'], key=lambda x: int(x['cpu']) + ) + return _sorted_data return [] - def get_volume_types(self, region_id): + def get_volume_types(self, region_id, provider_id): """Get Volume Types.""" if region_id not in self.regions: return [] - _url = "{0}/{1}".format( + _url = '{0}/cloud-providers/{1}/regions/{2}/volume-types'.format( self.BASE_URL, - 'cloud-providers/azure/regions/{0}/volume-types'.format(region_id)) + provider_id, + region_id) volume_types = [] resp = requests.get(_url, headers=self._get_headers()) if resp.status_code == 200 and resp.content: volume_resp = json.loads(resp.content) for value in volume_resp['data']: - volume_types.append({ - 'label': value['volumeTypeName'], - 'value': value['volumeTypeId'] - }) + if value['enabledInRegion']: + volume_types.append({ + 'label': value['volumeTypeName'], + 'value': value['volumeTypeId'], + 'supportedInstanceFamilyNames': value[ + 'supportedInstanceFamilyNames'] + }) return volume_types - def get_volume_properties(self, region_id, volume_type): + def get_volume_properties(self, region_id, provider_id, volume_type): """Get Volume Properties.""" if region_id not in self.regions: return [] - _url = "{0}/{1}".format( - self.BASE_URL, - 'cloud-providers/azure/regions/{0}/volume-types' - '/{1}/volume-properties'.format(region_id, volume_type)) + _url = '{0}/cloud-providers/{1}/regions/{2}/volume-types/{3}/' \ + 'volume-properties'.format(self.BASE_URL, + provider_id, + region_id, + volume_type) volume_properties = [] resp = requests.get(_url, headers=self._get_headers()) if resp.status_code == 200 and resp.content: @@ -394,13 +440,12 @@ def deploy_on_biganimal(data): _private_network = '1' if str(data['instance_details']['cloud_type'] ) == 'private' else '0' _instance_size = data['instance_details']['instance_size'].split('||')[1] - - cluster_arch = SINGLE_CLUSTER_ARCH nodes = 1 - if data['db_details']['high_availability']: - cluster_arch = HA_CLUSTER_ARCH - nodes = int(data['db_details']['replicas']) + nodes + if data['cluster_details']['cluster_type'] == HA_CLUSTER_ARCH: + nodes = int(data['cluster_details']['replicas']) + nodes + elif data['cluster_details']['cluster_type'] == EHA_CLUSTER_ARCH: + nodes = 5 args = [_cmd_script, data['cloud'], @@ -416,15 +461,25 @@ def deploy_on_biganimal(data): '--volume-type', str(data['instance_details']['volume_type']), '--volume-properties', - str(data['instance_details']['volume_properties']), + str(data['instance_details'].get('volume_properties', + data['instance_details'][ + 'volume_type'])), + '--volume-size', + str(data['instance_details'].get('volume_size', None)), + '--volume-IOPS', + str(data['instance_details'].get('volume_IOPS', None)), '--instance-type', str(_instance_size), '--private-network', _private_network, '--cluster-arch', - cluster_arch, + data['cluster_details']['cluster_type'], '--nodes', - str(nodes) + str(nodes), + '--replicas', + str(data['cluster_details']['replicas']), + '--cloud-provider', + str(data['cluster_details']['provider']), ] if 'biganimal_public_ip' in data['instance_details']: diff --git a/web/pgadmin/misc/cloud/rds/__init__.py b/web/pgadmin/misc/cloud/rds/__init__.py index 5cebb761d..d4d44cf38 100644 --- a/web/pgadmin/misc/cloud/rds/__init__.py +++ b/web/pgadmin/misc/cloud/rds/__init__.py @@ -58,6 +58,7 @@ blueprint = RDSModule(MODULE_NAME, __name__, @login_required def verify_credentials(): """Verify Credentials.""" + msg = '' data = json.loads(request.data, encoding='utf-8') session_token = data['secret']['session_token'] if\ diff --git a/web/pgadmin/misc/cloud/static/js/CloudWizard.jsx b/web/pgadmin/misc/cloud/static/js/CloudWizard.jsx index 3dc35e4a5..f5ad5cd8a 100644 --- a/web/pgadmin/misc/cloud/static/js/CloudWizard.jsx +++ b/web/pgadmin/misc/cloud/static/js/CloudWizard.jsx @@ -22,11 +22,13 @@ import pgAdmin from 'sources/pgadmin'; import {ToggleButtons, FinalSummary} from './cloud_components'; import { PrimaryButton } from '../../../../static/js/components/Buttons'; import {AwsCredentials, AwsInstanceDetails, AwsDatabaseDetails, validateCloudStep1, validateCloudStep2, validateCloudStep3} from './aws'; -import {BigAnimalInstance, BigAnimalDatabase, validateBigAnimal,validateBigAnimalStep2, validateBigAnimalStep3} from './biganimal'; +import {BigAnimalInstance, BigAnimalDatabase, BigAnimalClusterType, getProviderOptions, validateBigAnimal, validateBigAnimalStep2, validateBigAnimalStep3, validateBigAnimalStep4} from './biganimal'; import { isEmptyString } from 'sources/validators'; import { AWSIcon, BigAnimalIcon, AzureIcon } from '../../../../static/js/components/ExternalIcon'; import {AzureCredentials, AzureInstanceDetails, AzureDatabaseDetails, checkClusternameAvailbility, validateAzureStep2, validateAzureStep3} from './azure'; import EventBus from '../../../../static/js/helpers/EventBus'; +import { CLOUD_PROVIDERS } from './cloud_constants'; + const useStyles = makeStyles(() => ({ @@ -67,7 +69,7 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel}) const eventBus = React.useRef(new EventBus()); - let steps = [gettext('Cloud Provider'), gettext('Credentials'), + let steps = [gettext('Cloud Provider'), gettext('Credentials'), gettext('Cluster Type'), gettext('Instance Specification'), gettext('Database Details'), gettext('Review')]; const [currentStep, setCurrentStep] = React.useState(''); const [selectionVal, setCloudSelection] = React.useState(''); @@ -81,6 +83,8 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel}) const [verificationIntiated, setVerificationIntiated] = React.useState(false); const [bigAnimalInstanceData, setBigAnimalInstanceData] = React.useState({}); const [bigAnimalDatabaseData, setBigAnimalDatabaseData] = React.useState({}); + const [bigAnimalClusterTypeData, setBigAnimalClusterTypeData] = React.useState({}); + const [bigAnimalProviders, setBigAnimalProviders] = React.useState({}); const [azureCredData, setAzureCredData] = React.useState({}); const [azureInstanceData, setAzureInstanceData] = React.useState({}); @@ -124,7 +128,7 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel}) let _url = url_for('cloud.deploy_on_cloud'), post_data = {}; - if (cloudProvider == 'rds') { + if (cloudProvider == CLOUD_PROVIDERS.RDS) { post_data = { gid: nodeInfo.server_group._id, cloud: cloudProvider, @@ -132,7 +136,7 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel}) instance_details:cloudInstanceDetails, db_details: cloudDBDetails }; - } else if(cloudProvider == 'azure'){ + } else if(cloudProvider == CLOUD_PROVIDERS.AZURE){ post_data = { gid: nodeInfo.server_group._id, secret: azureCredData, @@ -145,7 +149,8 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel}) post_data = { gid: nodeInfo.server_group._id, cloud: cloudProvider, - instance_details:bigAnimalInstanceData, + cluster_details: bigAnimalClusterTypeData, + instance_details: bigAnimalInstanceData, db_details: bigAnimalDatabaseData }; } @@ -165,54 +170,61 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel}) setCallRDSAPI(currentStep); let isError = (cloudProvider == ''); switch(cloudProvider) { - case 'rds': + case CLOUD_PROVIDERS.RDS: switch (currentStep) { case 0: - setCloudSelection('rds'); + setCloudSelection(CLOUD_PROVIDERS.RDS); break; case 1: isError = validateCloudStep1(cloudDBCred); break; case 2: - isError = validateCloudStep2(cloudInstanceDetails, hostIP); break; case 3: + isError = validateCloudStep2(cloudInstanceDetails, hostIP); + break; + case 4: isError = validateCloudStep3(cloudDBDetails, nodeInfo); break; default: break; } break; - case 'biganimal': + case CLOUD_PROVIDERS.BIGANIMAL: switch (currentStep) { case 0: - setCloudSelection('biganimal'); + setCloudSelection(CLOUD_PROVIDERS.BIGANIMAL); break; case 1: isError = !verificationIntiated; break; case 2: - isError = validateBigAnimalStep2(bigAnimalInstanceData); + isError = validateBigAnimalStep2(bigAnimalClusterTypeData); break; case 3: - isError = validateBigAnimalStep3(bigAnimalDatabaseData, nodeInfo); + isError = validateBigAnimalStep3(bigAnimalInstanceData); + break; + case 4: + isError = validateBigAnimalStep4(bigAnimalDatabaseData, nodeInfo); break; default: break; } break; - case 'azure': + case CLOUD_PROVIDERS.AZURE: switch (currentStep) { case 0: - setCloudSelection('azure'); + setCloudSelection(CLOUD_PROVIDERS.AZURE); break; case 1: isError = !verificationIntiated; break; case 2: - isError = validateAzureStep2(azureInstanceData); break; case 3: + isError = validateAzureStep2(azureInstanceData); + break; + case 4: isError = validateAzureStep3(azureDatabaseData, nodeInfo); break; default: @@ -223,9 +235,18 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel}) return isError; }; + const onBeforeBack = (activeStep) => { + return new Promise((resolve)=>{ + if(activeStep == 3 && (cloudProvider == CLOUD_PROVIDERS.RDS || cloudProvider == CLOUD_PROVIDERS.AZURE)) { + resolve(true); + } + resolve(); + }); + }; + const onBeforeNext = (activeStep) => { return new Promise((resolve, reject)=>{ - if(activeStep == 1 && cloudProvider == 'rds') { + if(activeStep == 1 && cloudProvider == CLOUD_PROVIDERS.RDS) { setErrMsg([MESSAGE_TYPE.INFO, gettext('Validating credentials...')]); let _url = url_for('rds.verify_credentials'); const post_data = { @@ -239,14 +260,18 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel}) reject(); } else { setErrMsg(['', '']); - resolve(); + if (activeStep == 1) { + resolve(true); + } else { + resolve(false); + } } }) .catch(() => { setErrMsg([MESSAGE_TYPE.ERROR, gettext('Error while checking cloud credentials')]); reject(); }); - } else if(activeStep == 0 && cloudProvider == 'biganimal') { + } else if(activeStep == 0 && cloudProvider == CLOUD_PROVIDERS.BIGANIMAL) { if (!isEmptyString(verificationURI)) { resolve(); return; } setErrMsg([MESSAGE_TYPE.INFO, gettext('Getting EDB BigAnimal verification URL...')]); validateBigAnimal() @@ -260,20 +285,37 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel}) setErrMsg([MESSAGE_TYPE.ERROR, gettext(error)]); reject(); }); - } else if(activeStep == 2 && cloudProvider == 'azure'){ - setErrMsg([MESSAGE_TYPE.INFO, gettext('Checking cluster name availability...')]); - checkClusternameAvailbility(azureInstanceData.name) + } else if (activeStep == 1 && cloudProvider == CLOUD_PROVIDERS.BIGANIMAL ) { + getProviderOptions() .then((res)=>{ - if (res.data && res.data.success == 0 ) { - setErrMsg([MESSAGE_TYPE.ERROR, gettext('Specified cluster name is already used.')]); - }else{ - setErrMsg(['', '']); - } + setBigAnimalProviders(res); + setErrMsg(['', '']); resolve(); }).catch((error)=>{ setErrMsg([MESSAGE_TYPE.ERROR, gettext(error)]); reject(); }); + } else if (cloudProvider == CLOUD_PROVIDERS.AZURE) { + if (activeStep == 1) { + // Skip the current step + resolve(true); + } else if (activeStep == 2) { + setErrMsg([MESSAGE_TYPE.INFO, gettext('Checking cluster name availability...')]); + checkClusternameAvailbility(azureInstanceData.name) + .then((res)=>{ + if (res.data && res.data.success == 0 ) { + setErrMsg([MESSAGE_TYPE.ERROR, gettext('Specified cluster name is already used.')]); + }else{ + setErrMsg(['', '']); + } + resolve(); + }).catch((error)=>{ + setErrMsg([MESSAGE_TYPE.ERROR, gettext(error)]); + reject(); + }); + } else { + resolve(); + } } else { setErrMsg(['', '']); @@ -333,9 +375,9 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel}) setErrMsg([]); }); - let cloud_providers = [{label: gettext('Amazon RDS'), value: 'rds', icon: }, - {label: gettext('EDB BigAnimal'), value: 'biganimal', icon: }, - {'label': gettext('Azure PostgreSQL'), value: 'azure', icon: }]; + let cloud_providers = [{label: gettext('Amazon RDS'), value: CLOUD_PROVIDERS.RDS, icon: }, + {label: gettext('EDB BigAnimal'), value: CLOUD_PROVIDERS.BIGANIMAL, icon: }, + {'label': gettext('Azure PostgreSQL'), value: CLOUD_PROVIDERS.AZURE, icon: }]; return ( @@ -347,7 +389,8 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel}) onStepChange={wizardStepChange} onSave={onSave} onHelp={onDialogHelp} - beforeNext={onBeforeNext}> + beforeNext={onBeforeNext} + beforeBack={onBeforeBack}> {gettext('Select a cloud provider.')} @@ -361,39 +404,51 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel}) - {cloudProvider == 'biganimal' && + {cloudProvider == CLOUD_PROVIDERS.BIGANIMAL && {gettext('The verification code to authenticate the pgAdmin to EDB BigAnimal is: ')} {verificationCode}
{gettext('By clicking the below button, you will be redirected to the EDB BigAnimal authentication page in a new tab.')}
} - {cloudProvider == 'biganimal' && + {cloudProvider == CLOUD_PROVIDERS.BIGANIMAL && {gettext('Click here to authenticate yourself to EDB BigAnimal')} } - {cloudProvider == 'biganimal' && + {cloudProvider == CLOUD_PROVIDERS.BIGANIMAL && } - {cloudProvider == 'rds' && } + {cloudProvider == CLOUD_PROVIDERS.RDS && } - {cloudProvider == 'azure' && } + {cloudProvider == CLOUD_PROVIDERS.AZURE && }
- {cloudProvider == 'rds' && callRDSAPI == 2 && } + + + + {cloudProvider == CLOUD_PROVIDERS.RDS && callRDSAPI == 3 && } - {cloudProvider == 'biganimal' && callRDSAPI == 2 && } - {cloudProvider == 'azure' && callRDSAPI == 2 && } - - {cloudProvider == 'rds' && + {cloudProvider == CLOUD_PROVIDERS.RDS && } - {cloudProvider == 'biganimal' && callRDSAPI == 3 && } - {cloudProvider == 'azure' && } - + {gettext('Please review the details before creating the cloud instance.')} - {cloudProvider == 'rds' && callRDSAPI == 4 && } - {cloudProvider == 'biganimal' && callRDSAPI == 4 && } - {cloudProvider == 'azure' && callRDSAPI == 4 && { + if (source[0] == 'db_instance_class') { + return {reload_instances: false}; + } else { + state.instanceData = []; + return {reload_instances: true}; + } + }, + type: (state) => { + return { + type: 'select', + options: ()=>this.getInstances(state.db_version, + state.reload_instances, state.instanceData), + optionsLoaded: (options) => { state.instanceData = options; }, + optionsReloadBasis: state.db_version + (state.db_instance_class || 'm'), + controlProps: { + allowClear: false, + filter: (options) => { + if (options.length == 0) return; + let pattern = 'db.m'; + let pattern_1 = 'db.m'; + + if (state.db_instance_class) { + pattern = 'db.' + state.db_instance_class; + pattern_1 = 'db.' + state.db_instance_class; + } + if (state.db_instance_class == 'x') { + pattern_1 = 'db.' + 'r'; + } + return options.filter((option) => { + return (option.value.includes(pattern) || option.value.includes(pattern_1)); + }); + }, + } + }; + }, + }]; + } +} + + +export class StorageSchema extends BaseUISchema { + constructor() { + super({ + storage_type: 'io1', + storage_size: 100, + storage_IOPS: 3000, + storage_msg: 'Minimum: 20 GiB. Maximum: 16,384 GiB.' + }); + } + + get baseFields() { + return [ + { + id: 'storage_type', label: gettext('Storage type'), type: 'select', + mode: ['create'], + options: [ + {'label': gettext('General Purpose SSD (gp2)'), 'value': 'gp2'}, + {'label': gettext('Provisioned IOPS SSD (io1)'), 'value': 'io1'}, + {'label': gettext('Magnetic'), 'value': 'standard'} + ], noEmpty: true, + },{ + id: 'storage_size', label: gettext('Allocated storage'), type: 'text', + mode: ['create'], noEmpty: true, deps: ['storage_type'], + depChange: (state, source)=> { + if (source[0] !== 'storage_size') + if(state.storage_type === 'io1') { + return {storage_size: 100}; + } else if(state.storage_type === 'gp2') { + return {storage_size: 20}; + } else { + return {storage_size: 5}; + } + }, + helpMessage: gettext('Size in GiB.') + }, { + id: 'storage_IOPS', label: gettext('Provisioned IOPS'), type: 'text', + mode: ['create'], + visible: (state) => { + return state.storage_type === 'io1'; + } , deps: ['storage_type'], + depChange: (state, source) => { + if (source[0] !== 'storage_IOPS') { + if(state.storage_type === 'io1') { + return {storage_IOPS: 3000}; + } + } + }, + }, + ]; + } +} + +export class HighAvailablity extends BaseUISchema { + constructor() { + super({ + high_availability: false + }); + } + get baseFields() { + return [ + { + id: 'high_availability', + label: gettext('High availability'), + type: 'switch', + mode: ['create'], + helpMessage: gettext( + 'Creates a standby in a different Availability Zone (AZ) to provide data redundancy, eliminate I/O freezes, and minimize latency spikes during system backups.' + ), + }, + ]; + } +} + +export { + CloudInstanceDetailsSchema, + CloudDBCredSchema, + DatabaseSchema +}; diff --git a/web/pgadmin/misc/cloud/static/js/biganimal.js b/web/pgadmin/misc/cloud/static/js/biganimal.js index 1103c1976..b4f5d9dd7 100644 --- a/web/pgadmin/misc/cloud/static/js/biganimal.js +++ b/web/pgadmin/misc/cloud/static/js/biganimal.js @@ -10,59 +10,143 @@ import React from 'react'; import pgAdmin from 'sources/pgadmin'; import { getNodeAjaxOptions, getNodeListById } from 'pgbrowser/node_ajax'; -import {BigAnimalClusterSchema, BigAnimalDatabaseSchema} from './cloud_db_details_schema.ui'; +import {BigAnimalClusterSchema, BigAnimalDatabaseSchema, BigAnimalClusterTypeSchema} from './biganimal_schema.ui'; import SchemaView from '../../../../static/js/SchemaView'; import url_for from 'sources/url_for'; import getApiInstance from '../../../../static/js/api_instance'; import { isEmptyString } from 'sources/validators'; import PropTypes from 'prop-types'; import gettext from 'sources/gettext'; +import { makeStyles } from '@material-ui/core/styles'; +import { AWSIcon, MSAzureIcon } from '../../../../static/js/components/ExternalIcon'; + + +const useStyles = makeStyles(() => + ({ + providerHeight: { + height: '5em', + }, + AwsIcon: { + width: '6rem', + } + }), +); const axiosApi = getApiInstance(); +export function getProviderOptions() { + return new Promise((resolve, reject) => { + axiosApi.get(url_for('biganimal.providers')) + .then((res) => { + if (res.data.data) { + let _options= [], + _options_label = {'azure': , + 'aws': }; + _.forEach(res.data.data, (val) => { + _options.push({ + 'label': _options_label[val['value']], + 'value': val['value'], + 'disabled': !val['connected'] + }); + }); + resolve(_options); + } + }) + .catch((error) => { + reject(gettext(`Error while getting the biganimal providers: ${error.response.data.errormsg}`)); + }); + }); +} + +// BigAnimal Cluster Type +export function BigAnimalClusterType(props) { + const [bigAnimalClusterType, setBigAnimalClusterType] = React.useState(); + const classes = useStyles(); + + React.useMemo(() => { + const bigAnimalClusterTypeSchema = new BigAnimalClusterTypeSchema({ + providers: ()=>getNodeAjaxOptions('biganimal_providers', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData, { + useCache:false, + cacheNode: 'server', + customGenerateUrl: ()=>{ + return url_for('biganimal.providers'); + } + }), + }, { + nodeInfo: props.nodeInfo, + nodeData: props.nodeData, + hostIP: props.hostIP, + classes: classes, + bigAnimalProviders: props.bigAnimalProviders, + }); + setBigAnimalClusterType(bigAnimalClusterTypeSchema); + }, [props.cloudProvider]); + + return { /*This is intentional (SonarQube)*/ }} + viewHelperProps={{ mode: 'create' }} + schema={bigAnimalClusterType} + showFooter={false} + isTabView={false} + onDataChange={(isChanged, changedData) => { + props.setBigAnimalClusterTypeData(changedData); + }} + />; +} +BigAnimalClusterType.propTypes = { + nodeInfo: PropTypes.object, + nodeData: PropTypes.object, + cloudProvider: PropTypes.string, + setBigAnimalClusterTypeData: PropTypes.func, + hostIP: PropTypes.string, + bigAnimalProviders: PropTypes.object, +}; + + // BigAnimal Instance export function BigAnimalInstance(props) { const [bigAnimalInstance, setBigAnimalInstance] = React.useState(); React.useMemo(() => { const bigAnimalSchema = new BigAnimalClusterSchema({ - regions: ()=>getNodeAjaxOptions('biganimal_regions', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData, { + regions: (provider_id)=>getNodeAjaxOptions('biganimal_regions', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData, { useCache:false, cacheNode: 'server', customGenerateUrl: ()=>{ - return url_for('biganimal.regions'); + return url_for('biganimal.regions', {'provider_id': provider_id || 0}); } }), - instance_types: (region_id)=>{ + instance_types: (region_id, provider_id)=>{ if (isEmptyString(region_id)) return []; return getNodeAjaxOptions('biganimal_instance_types', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData, { useCache:false, cacheNode: 'server', customGenerateUrl: ()=>{ - return url_for('biganimal.instance_types', {'region_id': region_id || 0}); + return url_for('biganimal.instance_types', {'region_id': region_id || 0, 'provider_id': provider_id || 0}); } }); }, - volume_types: (region_id)=>{ + volume_types: (region_id, provider_id)=>{ if (isEmptyString(region_id)) return []; return getNodeAjaxOptions('biganimal_volume_types', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData, { useCache:false, cacheNode: 'server', customGenerateUrl: ()=>{ - return url_for('biganimal.volume_types', {'region_id': region_id || 0}); + return url_for('biganimal.volume_types', {'region_id': region_id || 0, 'provider_id': provider_id || 0}); } }); }, - volume_properties: (region_id, volume_type)=>{ + volume_properties: (region_id, provider_id, volume_type)=>{ if (isEmptyString(region_id) || isEmptyString(volume_type)) return []; return getNodeAjaxOptions('biganimal_volume_properties', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData, { useCache:false, cacheNode: 'server', customGenerateUrl: ()=>{ - return url_for('biganimal.volume_properties', {'region_id': region_id || 0, 'volume_type': volume_type || ''}); + return url_for('biganimal.volume_properties', {'region_id': region_id || 0, 'provider_id': provider_id || 0, 'volume_type': volume_type || ''}); } }); }, @@ -70,6 +154,7 @@ export function BigAnimalInstance(props) { nodeInfo: props.nodeInfo, nodeData: props.nodeData, hostIP: props.hostIP, + provider: props.bigAnimalClusterTypeData.provider, }); setBigAnimalInstance(bigAnimalSchema); }, [props.cloudProvider]); @@ -92,10 +177,11 @@ BigAnimalInstance.propTypes = { cloudProvider: PropTypes.string, setBigAnimalInstanceData: PropTypes.func, hostIP: PropTypes.string, + bigAnimalClusterTypeData: PropTypes.object, }; -// BigAnimal Instance +// BigAnimal Database export function BigAnimalDatabase(props) { const [bigAnimalDatabase, setBigAnimalDatabase] = React.useState(); @@ -108,15 +194,18 @@ export function BigAnimalDatabase(props) { return url_for('biganimal.db_types'); } }), - db_versions: (db_type)=>getNodeAjaxOptions('biganimal_db_versions', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData, { + db_versions: (cluster_type, pg_type)=>getNodeAjaxOptions('biganimal_db_versions', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData, { useCache:false, cacheNode: 'server', customGenerateUrl: ()=>{ - return url_for('biganimal.db_versions', {'db_type': db_type || 'pg'}); + return url_for('biganimal.db_versions', {'cluster_type': cluster_type || 'single', 'pg_type': pg_type || 'pg'}); } }), server_groups: ()=>getNodeListById(pgAdmin.Browser.Nodes['server_group'], props.nodeInfo, props.nodeData), - }, {gid: props.nodeInfo['server_group']._id}); + }, { + gid: props.nodeInfo['server_group']._id, + cluster_type: props.bigAnimalClusterTypeData.cluster_type, + }); setBigAnimalDatabase(bigAnimalDBSchema); }, [props.cloudProvider]); @@ -137,12 +226,13 @@ BigAnimalDatabase.propTypes = { nodeData: PropTypes.object, cloudProvider: PropTypes.string, setBigAnimalDatabaseData: PropTypes.func, + bigAnimalClusterTypeData: PropTypes.object }; export function validateBigAnimal() { return new Promise((resolve, reject)=>{ - let _url = url_for('biganimal.verification') ; + let _url = url_for('biganimal.verification'); axiosApi.get(_url) .then((res) => { if (res.data.data) { @@ -162,7 +252,7 @@ function createData(name, value) { return { name, value }; } -export function getBigAnimalSummary(cloud, bigAnimalInstanceData, bigAnimalDatabaseData) { +export function getBigAnimalSummary(cloud, bigAnimalClusterTypeData, bigAnimalInstanceData, bigAnimalDatabaseData) { const rows1 = [ createData(gettext('Cloud'), cloud), createData(gettext('Instance name'), bigAnimalInstanceData.name), @@ -174,49 +264,72 @@ export function getBigAnimalSummary(cloud, bigAnimalInstanceData, bigAnimalDatab let instance_size = bigAnimalInstanceData.instance_size.split('||'); const rows2 = [ + createData(gettext('Cluster type'), bigAnimalClusterTypeData.cluster_type), + createData(gettext('No. of Standby Replicas'), bigAnimalClusterTypeData.replicas), + createData(gettext('Provider'), bigAnimalClusterTypeData.provider), + ]; + + const rows3 = [ createData(gettext('Instance type'), bigAnimalInstanceData.instance_type), createData(gettext('Instance series'), bigAnimalInstanceData.instance_series), createData(gettext('Instance size'), instance_size[0]), ]; - const rows3 = [ + const rows4 = [ createData(gettext('Volume type'), bigAnimalInstanceData.volume_type), - createData(gettext('Volume properties'), bigAnimalInstanceData.volume_properties), + createData(gettext('Volume size'), bigAnimalInstanceData.volume_size), + createData(gettext('Volume IOPS'), bigAnimalInstanceData.volume_IOPS), ]; - const rows4 = [ + const rows5 = [ createData(gettext('Password'), 'xxxxxxx'), createData(gettext('Database Type'), bigAnimalDatabaseData.database_type), createData(gettext('Database Version'), bigAnimalDatabaseData.postgres_version), - createData(gettext('High Availability'), bigAnimalDatabaseData.high_availability), - createData(gettext('No. of Standby Replicas'), bigAnimalDatabaseData.replicas), ]; - return [rows1, rows2, rows3, rows4]; + return [rows1, rows2, rows3, rows4, rows5]; } -export function validateBigAnimalStep2(cloudInstanceDetails) { +export function validateBigAnimalStep2(cloudTypeDetails) { let isError = false; - if (isEmptyString(cloudInstanceDetails.name) || - isEmptyString(cloudInstanceDetails.region) || isEmptyString(cloudInstanceDetails.instance_type) || - isEmptyString(cloudInstanceDetails.instance_series)|| isEmptyString(cloudInstanceDetails.instance_size) || - isEmptyString(cloudInstanceDetails.volume_type)|| isEmptyString(cloudInstanceDetails.volume_properties)) { + if (isEmptyString(cloudTypeDetails.cluster_type) || isEmptyString(cloudTypeDetails.provider) || + (cloudTypeDetails.cluster_type == 'ha' && cloudTypeDetails.replicas == 0) + ) { isError = true; } return isError; } -export function validateBigAnimalStep3(cloudDBDetails, nodeInfo) { +export function validateBigAnimalStep3(cloudDetails) { + let isError = false; + if (isEmptyString(cloudDetails.name) || + isEmptyString(cloudDetails.region) || isEmptyString(cloudDetails.instance_type) || + isEmptyString(cloudDetails.instance_series)|| isEmptyString(cloudDetails.instance_size) || + isEmptyString(cloudDetails.volume_type) || (cloudDetails.provider != 'aws' && isEmptyString(cloudDetails.volume_properties)) ) { + isError = true; + } + + if (cloudDetails.provider == 'aws') { + if (isEmptyString(cloudDetails.volume_IOPS) || isEmptyString(cloudDetails.volume_IOPS) || (cloudDetails.volume_IOPS != 'io2' && + (cloudDetails.volume_size < 1 || cloudDetails.volume_size > 16384)) || + (cloudDetails.volume_IOPS == 'io2' && (cloudDetails.volume_size < 4 || cloudDetails.volume_size > 16384)) || + (cloudDetails.volume_IOPS != 'io2' && cloudDetails.volume_IOPS != 3000) || + (cloudDetails.volume_IOPS == 'io2' && (cloudDetails.volume_IOPS < 100 || cloudDetails.volume_IOPS > 2000))) { + isError = true; + } + } + + return isError; +} + +export function validateBigAnimalStep4(cloudDBDetails, nodeInfo) { let isError = false; if (isEmptyString(cloudDBDetails.password) || isEmptyString(cloudDBDetails.database_type) || isEmptyString(cloudDBDetails.postgres_version)) { isError = true; } - if(cloudDBDetails.high_availability && (isEmptyString(cloudDBDetails.replicas) || cloudDBDetails.replicas <= 0)) { - isError = true; - } if (isEmptyString(cloudDBDetails.gid)) cloudDBDetails.gid = nodeInfo['server_group']._id; return isError; } diff --git a/web/pgadmin/misc/cloud/static/js/biganimal_schema.ui.js b/web/pgadmin/misc/cloud/static/js/biganimal_schema.ui.js new file mode 100644 index 000000000..980ddff61 --- /dev/null +++ b/web/pgadmin/misc/cloud/static/js/biganimal_schema.ui.js @@ -0,0 +1,468 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2022, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; +import { isEmptyString } from 'sources/validators'; +import { CLOUD_PROVIDERS } from './cloud_constants'; + +class BigAnimalClusterTypeSchema extends BaseUISchema { + + constructor(fieldOptions = {}, initValues = {}) { + super({ + oid: undefined, + cluster_type: '', + replicas: 0, + provider: '', + ...initValues + }); + + this.fieldOptions = { + ...fieldOptions, + }; + this.initValues = initValues; + } + + get idAttribute() { + return 'oid'; + } + + validate(data, setErrMsg) { + if (data.cluster_type == 'ha' && data.replicas == 0) { + setErrMsg('replicas', gettext('Please select number of stand by replicas.')); + return true; + } + return false; + } + + get baseFields() { + return [ + { + id: 'cluster_type', label: gettext('Cluster type'), noEmpty: true, + type: () => { + return { + type: 'toggle', + options: [ + {'label': gettext('Single Node'), value: 'single'}, + {'label': gettext('High Availability'), value: 'ha'}, + {'label': gettext('Extreme High Availability'), value: 'eha'}, + ], + }; + }, + }, { + id: 'replicas', label: gettext('Number of standby replicas'), type: 'select', + mode: ['create'], deps: ['cluster_type'], + controlProps: { allowClear: false }, + helpMessage: gettext('Adding standby replicas will increase your number of CPUs, as well as your cost.'), + options: [ + {'label': gettext('1'), 'value': 1}, + {'label': gettext('2'), 'value': 2}, + ], + disabled: (state) => { + return state.cluster_type != 'ha'; + } + }, { id: 'provider', label: gettext('Cluster provider'), noEmpty: true, + type: 'toggle', className: this.initValues.classes.providerHeight, + options: this.initValues.bigAnimalProviders, + }, + ]; + } +} + +class BigAnimalInstanceSchema extends BaseUISchema { + constructor(fieldOptions = {}, initValues={}) { + super({ + oid: undefined, + instance_type: '', + instance_series: '', + instance_size: '', + ...initValues + }); + + this.fieldOptions = { + ...fieldOptions, + }; + + this.initValues = initValues; + } + + get idAttribute() { + return 'oid'; + } + + get baseFields() { + return [ + { + id: 'instance_type', label: gettext('Instance type'), + mode: ['create'], + deps: [['region']], + type: (state) => { + return { + type: 'select', + options: ()=>this.fieldOptions.instance_types(state.region, state.provider), + optionsReloadBasis: state.region, + optionsLoaded: (options) => { state.instanceData = options; }, + controlProps: { + allowClear: false, + filter: (options) => { + if (options.length == 0) return; + let _types = _.uniq(_.map(options, 'category')), + _options = []; + _.forEach(_types, (region) => { + _options.push({ + 'label': region, + 'value': region + }); + }); + return _options; + }, + } + }; + }, + noEmpty: true, + },{ + id: 'instance_series', label: gettext('Instance series'), + mode: ['create'], deps: ['instance_type'], + type: (state) => { + return { + type: 'select', + options: state.instanceData, + optionsReloadBasis: state.instance_type, + controlProps: { + allowClear: false, + filter: (options) => { + if (options.length == 0) return; + let _types = _.filter(options, {'category': state.instance_type}), + _options = []; + _types = _.uniq(_.map(_types, 'familyName')); + _.forEach(_types, (value) => { + _options.push({ + 'label': value, + 'value': value + }); + }); + return _options; + }, + } + }; + }, + noEmpty: true, + },{ + id: 'instance_size', label: gettext('Instance size'), + mode: ['create'], deps: ['instance_series'], + type: (state) => { + return { + type: 'select', + options: state.instanceData, + optionsReloadBasis: state.instance_series, + controlProps: { + allowClear: false, + filter: (options) => { + if (options.length == 0) return; + let _types = _.filter(options, {'familyName': state.instance_series}), + _options = []; + _.forEach(_types, (value) => { + _options.push({ + 'label': value.instanceTypeName + ' (' + value.cpu + 'vCPU, ' + value.ram + 'GB RAM)', + 'value': value.instanceTypeName + ' (' + value.cpu + 'vCPU, ' + value.ram + 'GB RAM)' + '||' + value.instanceTypeId, + }); + }); + return _options; + }, + } + }; + }, noEmpty: true, + }, + ]; + } +} + + +class BigAnimalVolumeSchema extends BaseUISchema { + constructor(fieldOptions = {}, initValues = {}) { + super({ + oid: undefined, + volume_type: '', + volume_properties: '', + volume_size: 4, + volume_IOPS: '', + ...initValues + }); + + this.fieldOptions = { + ...fieldOptions, + }; + this.initValues = initValues; + this.volumeType = ''; + } + + get idAttribute() { + return 'oid'; + } + + validate(data, setErrMsg) { + if (data.provider != CLOUD_PROVIDERS.AWS && isEmptyString(data.volume_properties)) { + setErrMsg('replicas', gettext('Please select volume properties.')); + return true; + } + if (data.provider == CLOUD_PROVIDERS.AWS) { + if (isEmptyString(data.volume_IOPS)) { + setErrMsg('replicas', gettext('Please select volume IOPS.')); + return true; + } + if (!isEmptyString(data.volume_size)) { + if( data.volume_IOPS != 'io2' && (data.volume_size < 1 || data.volume_size > 16384)) { + setErrMsg('replicas', gettext('Please enter the volume size in the range between 1 tp 16384.')); + return true; + } + if (data.volume_IOPS == 'io2' && (data.volume_size < 4 || data.volume_size > 16384)) { + setErrMsg('replicas', gettext('Please enter the volume size in the range between 4 tp 16384.')); + return true; + } + } + if (!isEmptyString(data.volume_IOPS)) { + if(data.volume_IOPS != 'io2' && data.volume_IOPS != 3000) { + setErrMsg('replicas', gettext('Please enter the volume IOPS 3000.')); + return true; + } + if(data.volume_IOPS == 'io2' && (data.volume_IOPS < 100 || data.volume_IOPS > 2000)) { + setErrMsg('replicas', gettext('Please enter the volume IOPS in the range between 100 tp 2000.')); + return true; + } + } + } + return false; + } + + get baseFields() { + let obj = this; + return [ + { + id: 'volume_type', label: gettext('Volume type'), + mode: ['create'], deps: [['region'], ['instance_series']], + type: (state) => { + return { + type: 'select', + options: ()=>this.fieldOptions.volume_types(state.region, state.provider), + optionsReloadBasis: state.region, + controlProps: { + allowClear: false, + filter: (options) => { + if (options.length == 0) return; + return options.filter((option) => { + return (option.supportedInstanceFamilyNames.includes(state.instance_series)); + }); + }, + } + }; + }, noEmpty: true, + },{ + id: 'volume_properties', label: gettext('Volume properties'), + mode: ['create'], deps: ['volume_type', 'provider'], + type: (state) => { + return { + type: 'select', + options: ()=>this.fieldOptions.volume_properties(state.region, state.provider, state.volume_type), + optionsReloadBasis: state.volume_type, + }; + }, + visible: (state) => { + return state.provider !== CLOUD_PROVIDERS.AWS; + }, + }, { + id: 'volume_size', label: gettext('Size'), type: 'text', + mode: ['create'], noEmpty: true, deps: ['volume_type'], + depChange: (state, source)=> { + obj.volumeType = state.volume_type; + if (source[0] !== 'volume_size') { + if(state.volume_type == 'io2' || state.provider === CLOUD_PROVIDERS.AZURE) { + return {volume_size: 4}; + } else { + return {volume_size: 1}; + } + } + }, + visible: (state) => { + return state.provider === CLOUD_PROVIDERS.AWS; + }, + helpMessage: obj.volumeType == 'io2' ? gettext('Size (4-16,384 GiB)') : gettext('Size (1-16,384 GiB)') + }, { + id: 'volume_IOPS', label: gettext('IOPS'), type: 'text', + mode: ['create'], + helpMessage: obj.volumeType == 'io2' ? gettext('IOPS (100-2,000)') : gettext('IOPS (3,000-3,000)'), + visible: (state) => { + return state.provider === CLOUD_PROVIDERS.AWS; + }, deps: ['volume_type'], + depChange: (state, source) => { + obj.volumeType = state.volume_type; + if (source[0] !== 'volume_IOPS') { + if (state.provider === CLOUD_PROVIDERS.AWS) { + if(state.volume_type === 'io2') { + return {volume_IOPS: 100}; + } else { + return {volume_IOPS: 3000}; + } + } else { + return {volume_IOPS: 120}; + } + } + }, + }, + ]; + } +} + + +class BigAnimalDatabaseSchema extends BaseUISchema { + constructor(fieldOptions = {}, initValues = {}) { + super({ + oid: undefined, + password: '', + confirm_password: '', + database_type: '', + postgres_version: '', + high_availability: false, + replicas: 0, + ...initValues + }); + + this.fieldOptions = { + ...fieldOptions, + }; + this.initValues = initValues; + } + + + validate(data, setErrMsg) { + if(!isEmptyString(data.password) && !isEmptyString(data.confirm_password) + && data.password != data.confirm_password) { + setErrMsg('confirm_password', gettext('Passwords do not match.')); + return true; + } + if (!isEmptyString(data.confirm_password) && data.confirm_password.length < 12) { + setErrMsg('confirm_password', gettext('Password must be 12 characters or more.')); + return true; + } + if (data.high_availability && (isEmptyString(data.replicas) || data.replicas <= 0)) { + setErrMsg('replicas', gettext('Please select number of stand by replicas.')); + return true; + } + return false; + } + + get idAttribute() { + return 'oid'; + } + + get baseFields() { + return [ + { + id: 'gid', label: gettext('pgAdmin server group'), type: 'select', + options: this.fieldOptions.server_groups, + mode: ['create'], + controlProps: { allowClear: false }, + noEmpty: true, + }, { + id: 'database_type', label: gettext('Database type'), mode: ['create'], + type: 'select', + options: this.fieldOptions.db_types, + noEmpty: true, orientation: 'vertical', + },{ + id: 'postgres_version', label: gettext('Database version'), + mode: ['create'], noEmpty: true, deps: ['database_type'], + type: (state) => { + return { + type: 'select', + options: ()=>this.fieldOptions.db_versions(this.initValues.cluster_typ, state.database_type), + optionsReloadBasis: state.database_type, + }; + }, + },{ + id: 'password', label: gettext('Database password'), type: 'password', + mode: ['create'], noEmpty: true, + },{ + id: 'confirm_password', label: gettext('Confirm password'), type: 'password', + mode: ['create'], noEmpty: true, + }, + + ]; + } +} + + +class BigAnimalClusterSchema extends BaseUISchema { + constructor(fieldOptions = {}, initValues = {}) { + super({ + oid: undefined, + name: '', + region: '', + cloud_type: 'public', + biganimal_public_ip: initValues.hostIP, + ...initValues + }); + + this.fieldOptions = { + ...fieldOptions, + }; + this.initValues = initValues; + + this.instance_types = new BigAnimalInstanceSchema({ + instance_types: this.fieldOptions.instance_types, + }); + this.volume_types = new BigAnimalVolumeSchema({ + volume_types: this.fieldOptions.volume_types, + volume_properties: this.fieldOptions.volume_properties + }); + } + + get idAttribute() { + return 'oid'; + } + + get baseFields() { + return [ + { + id: 'name', label: gettext('Cluster name'), type: 'text', + mode: ['create'], noEmpty: true, + },{ + id: 'region', label: gettext('Region'), + controlProps: { allowClear: false }, + noEmpty: true, + mode: ['create'], + dep: [this.initValues.provider], + type: () => { + return { + type: 'select', + options: ()=>this.fieldOptions.regions(this.initValues.provider), + optionsReloadBasis: this.initValues.provider, + }; + }, + },{ + id: 'biganimal_public_ip', label: gettext('Public IP range'), type: 'text', + mode: ['create'], + helpMessage: gettext('IP address range for allowed inbound traffic, for example: 127.0.0.1/32. Add multiple IP addresses/ranges separated with commas. Leave blank for 0.0.0.0/0'), + },{ + type: 'nested-fieldset', label: gettext('Instance Type'), + mode: ['create'], deps: ['region'], + schema: this.instance_types, + },{ + type: 'nested-fieldset', label: gettext('Storage'), + mode: ['create'], deps: ['region'], + schema: this.volume_types, + } + ]; + } +} + + +export { + BigAnimalClusterSchema, + BigAnimalDatabaseSchema, + BigAnimalClusterTypeSchema +}; diff --git a/web/pgadmin/misc/cloud/static/js/cloud_components.jsx b/web/pgadmin/misc/cloud/static/js/cloud_components.jsx index ca9a16ee8..200f69b28 100644 --- a/web/pgadmin/misc/cloud/static/js/cloud_components.jsx +++ b/web/pgadmin/misc/cloud/static/js/cloud_components.jsx @@ -69,8 +69,8 @@ export function FinalSummary(props) { summaryHeader = ['Cloud Details', 'Version and Instance Details', 'Storage Details', 'Database Details']; if (props.cloudProvider == 'biganimal') { - summary = getBigAnimalSummary(props.cloudProvider, props.instanceData, props.databaseData); - summaryHeader[1] = 'Version Details'; + summary = getBigAnimalSummary(props.cloudProvider, props.clusterTypeData, props.instanceData, props.databaseData); + summaryHeader = ['Cloud Details', 'Cluster Details' ,'Version Details', 'Storage Details', 'Database Details']; } else if(props.cloudProvider == 'azure') { summaryHeader.push('Network Connectivity','Availability'); summary = getAzureSummary(props.cloudProvider, props.instanceData, props.databaseData); @@ -107,4 +107,5 @@ FinalSummary.propTypes = { cloudProvider: PropTypes.string, instanceData: PropTypes.object, databaseData: PropTypes.object, + clusterTypeData: PropTypes.object, }; diff --git a/web/pgadmin/misc/cloud/static/js/cloud_constants.js b/web/pgadmin/misc/cloud/static/js/cloud_constants.js new file mode 100644 index 000000000..6388c47d0 --- /dev/null +++ b/web/pgadmin/misc/cloud/static/js/cloud_constants.js @@ -0,0 +1,15 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2023, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +export const CLOUD_PROVIDERS = { + AZURE: 'azure', + BIGANIMAL: 'biganimal', + AWS: 'aws', + RDS: 'rds', +}; diff --git a/web/pgadmin/misc/cloud/static/js/cloud_db_details_schema.ui.js b/web/pgadmin/misc/cloud/static/js/cloud_db_details_schema.ui.js deleted file mode 100644 index 207f8fc2f..000000000 --- a/web/pgadmin/misc/cloud/static/js/cloud_db_details_schema.ui.js +++ /dev/null @@ -1,678 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2023, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -import gettext from 'sources/gettext'; -import BaseUISchema from 'sources/SchemaView/base_schema.ui'; -import { isEmptyString } from 'sources/validators'; - - -class CloudInstanceDetailsSchema extends BaseUISchema { - constructor(fieldOptions = {}, initValues = {}) { - super({ - oid: undefined, - name: '', - public_ip: initValues.hostIP, - high_availability: false, - ...initValues - }); - - this.fieldOptions = { - ...fieldOptions, - }; - this.initValues = initValues; - } - - get idAttribute() { - return 'oid'; - } - - get baseFields() { - return [ - { - id: 'name', label: gettext('Instance name'), type: 'text', - mode: ['create'], noEmpty: true, - }, { - id: 'public_ip', label: gettext('Public IP range'), type: 'text', - mode: ['create'], - helpMessage: gettext('IP address range for allowed inbound traffic, for example: 127.0.0.1/32. Add multiple IP addresses/ranges separated with commas.'), - }, { - type: 'nested-fieldset', label: gettext('Version & Instance'), - mode: ['create'], - schema: new InstanceSchema(this.fieldOptions.version, - this.fieldOptions.instance_type, - this.fieldOptions.getInstances), - }, { - type: 'nested-fieldset', label: gettext('Storage'), - mode: ['create'], - schema: new StorageSchema(), - }, { - type: 'nested-fieldset', label: gettext('Availability'), - mode: ['create'], - schema: new HighAvailablity(), - }, - ]; - } -} - - -class CloudDBCredSchema extends BaseUISchema { - constructor(fieldOptions = {}, initValues = {}) { - super({ - oid: null, - region: '', - access_key: '', - secret_access_key: '', - session_token: '', - is_valid_cred: false, - ...initValues - }); - - this.fieldOptions = { - ...fieldOptions, - }; - - } - - get idAttribute() { - return 'oid'; - } - - get baseFields() { - return [ - { - id: 'region', label: gettext('Region'), - type: 'select', - options: this.fieldOptions.regions, - controlProps: { allowClear: false }, - noEmpty: true, - helpMessage: gettext('The cloud instance will be deployed in the selected region.') - },{ - id: 'access_key', label: gettext('AWS access key'), type: 'text', - mode: ['create'], noEmpty: true, - }, { - id: 'secret_access_key', label: gettext('AWS secret access key'), type: 'password', - mode: ['create'], noEmpty: true, - }, { - id: 'session_token', label: gettext('AWS session token'), type: 'multiline', - mode: ['create'], noEmpty: false, - helpMessage: gettext('Temporary AWS session required session token.') - } - ]; - } -} - - -class DatabaseSchema extends BaseUISchema { - constructor(fieldOptions = {}, initValues={}) { - super({ - oid: undefined, - gid: undefined, - db_name: '', - db_username: '', - db_password: '', - db_confirm_password: '', - db_port: 5432, - ...initValues, - }); - - this.fieldOptions = { - ...fieldOptions, - }; - - } - - validate(data, setErrMsg) { - if(!isEmptyString(data.db_password) && !isEmptyString(data.db_confirm_password) - && data.db_password != data.db_confirm_password) { - setErrMsg('db_confirm_password', gettext('Passwords do not match.')); - return true; - } - if (!isEmptyString(data.db_confirm_password) && data.db_confirm_password.length < 8) { - setErrMsg('db_confirm_password', gettext('Password must be 8 characters or more.')); - return true; - } - if (data.db_confirm_password.includes('\'') || data.db_confirm_password.includes('"') || - data.db_confirm_password.includes('@') || data.db_confirm_password.includes('/')) { - setErrMsg('db_confirm_password', gettext('Invalid passowrd.')); - return true; - } - - return false; - } - - get idAttribute() { - return 'oid'; - } - - get baseFields() { - return [{ - id: 'gid', label: gettext('pgAdmin server group'), type: 'select', - options: this.fieldOptions.server_groups, - mode: ['create'], - controlProps: { allowClear: false }, - noEmpty: true, - }, { - id: 'db_name', label: gettext('Database name'), type: 'text', - mode: ['create'], noEmpty: true, - }, { - id: 'db_username', label: gettext('Username'), type: 'text', - mode: ['create'], noEmpty: true, - }, { - id: 'db_password', label: gettext('Password'), type: 'password', - mode: ['create'], noEmpty: true, - helpMessage: gettext('At least 8 printable ASCII characters. Cannot contain any of the following: / \(slash\), \'\(single quote\), "\(double quote\) and @ \(at sign\).') - }, { - id: 'db_confirm_password', label: gettext('Confirm password'), - type: 'password', - mode: ['create'], noEmpty: true, - }, { - id: 'db_port', label: gettext('Port'), type: 'text', - mode: ['create'], noEmpty: true, - }]; - } -} - - -export class InstanceSchema extends BaseUISchema { - constructor(versionOpts, instanceOpts, getInstances) { - super({ - db_version: '', - db_instance_class: 'm', - instance_type: '', - reload_instances: true, - }); - this.versionOpts = versionOpts; - this.instanceOpts = instanceOpts; - this.getInstances = getInstances; - this.instanceData = []; - } - - get baseFields() { - return [{ - id: 'db_version', label: gettext('Database version'), - type: 'select', - options: this.versionOpts, - controlProps: { allowClear: false }, - deps: ['name'], - noEmpty: true, - },{ - id: 'db_instance_class', label: gettext('Instance class'), - type: 'select', - options: [ - {'label': gettext('Standard classes (includes m classes)'), value: 'm'}, - {'label': gettext('Memory optimized classes (includes r & x classes)'), value: 'x'}, - {'label': gettext('Burstable classes (includes t classes)'), value: 't'}, - ], noEmpty: true - },{ - id: 'instance_type', label: gettext('Instance type'), - options: this.instanceOpts, - deps: ['db_version', 'db_instance_class'], - depChange: (state, source)=> { - if (source[0] == 'db_instance_class') { - return {reload_instances: false}; - } else { - state.instanceData = []; - return {reload_instances: true}; - } - }, - type: (state) => { - return { - type: 'select', - options: ()=>this.getInstances(state.db_version, - state.reload_instances, state.instanceData), - optionsLoaded: (options) => { state.instanceData = options; }, - optionsReloadBasis: state.db_version + (state.db_instance_class || 'm'), - controlProps: { - allowClear: false, - filter: (options) => { - if (options.length == 0) return; - let pattern = 'db.m'; - let pattern_1 = 'db.m'; - - if (state.db_instance_class) { - pattern = 'db.' + state.db_instance_class; - pattern_1 = 'db.' + state.db_instance_class; - } - if (state.db_instance_class == 'x') { - pattern_1 = 'db.' + 'r'; - } - return options.filter((option) => { - return (option.value.includes(pattern) || option.value.includes(pattern_1)); - }); - }, - } - }; - }, - }]; - } -} - - -export class StorageSchema extends BaseUISchema { - constructor() { - super({ - storage_type: 'io1', - storage_size: 100, - storage_IOPS: 3000, - storage_msg: 'Minimum: 20 GiB. Maximum: 16,384 GiB.' - }); - } - - get baseFields() { - return [ - { - id: 'storage_type', label: gettext('Storage type'), type: 'select', - mode: ['create'], - options: [ - {'label': gettext('General Purpose SSD (gp2)'), 'value': 'gp2'}, - {'label': gettext('Provisioned IOPS SSD (io1)'), 'value': 'io1'}, - {'label': gettext('Magnetic'), 'value': 'standard'} - ], noEmpty: true, - },{ - id: 'storage_size', label: gettext('Allocated storage'), type: 'text', - mode: ['create'], noEmpty: true, deps: ['storage_type'], - depChange: (state, source)=> { - if (source[0] !== 'storage_size') - if(state.storage_type === 'io1') { - return {storage_size: 100}; - } else if(state.storage_type === 'gp2') { - return {storage_size: 20}; - } else { - return {storage_size: 5}; - } - }, - helpMessage: gettext('Size in GiB.') - }, { - id: 'storage_IOPS', label: gettext('Provisioned IOPS'), type: 'text', - mode: ['create'], - visible: (state) => { - return state.storage_type === 'io1'; - } , deps: ['storage_type'], - depChange: (state, source) => { - if (source[0] !== 'storage_IOPS') { - if(state.storage_type === 'io1') { - return {storage_IOPS: 3000}; - } - } - }, - }, - ]; - } -} - -export class HighAvailablity extends BaseUISchema { - constructor() { - super({ - high_availability: false - }); - } - get baseFields() { - return [ - { - id: 'high_availability', - label: gettext('High availability'), - type: 'switch', - mode: ['create'], - helpMessage: gettext( - 'Creates a standby in a different Availability Zone (AZ) to provide data redundancy, eliminate I/O freezes, and minimize latency spikes during system backups.' - ), - }, - ]; - } -} - - -class BigAnimalInstanceSchema extends BaseUISchema { - constructor(fieldOptions = {}, initValues={}) { - super({ - oid: undefined, - instance_type: '', - instance_series: '', - instance_size: '', - ...initValues - }); - - this.fieldOptions = { - ...fieldOptions, - }; - - this.initValues = initValues; - } - - get idAttribute() { - return 'oid'; - } - - get baseFields() { - return [ - { - id: 'instance_type', label: gettext('Instance type'), - mode: ['create'], - deps: [['region']], - type: (state) => { - return { - type: 'select', - options: ()=>this.fieldOptions.instance_types(state.region), - optionsReloadBasis: state.region, - optionsLoaded: (options) => { state.instanceData = options; }, - controlProps: { - allowClear: false, - filter: (options) => { - if (options.length == 0) return; - let _types = _.uniq(_.map(options, 'category')), - _options = []; - _.forEach(_types, (region) => { - _options.push({ - 'label': region, - 'value': region - }); - }); - return _options; - }, - } - }; - }, - noEmpty: true, - },{ - id: 'instance_series', label: gettext('Instance series'), - mode: ['create'], deps: ['instance_type'], - type: (state) => { - return { - type: 'select', - options: state.instanceData, - optionsReloadBasis: state.instance_type, - controlProps: { - allowClear: false, - filter: (options) => { - if (options.length == 0) return; - let _types = _.filter(options, {'category': state.instance_type}), - _options = []; - _types = _.uniq(_.map(_types, 'familyName')); - _.forEach(_types, (value) => { - _options.push({ - 'label': value, - 'value': value - }); - }); - return _options; - }, - } - }; - }, - noEmpty: true, - },{ - id: 'instance_size', label: gettext('Instance size'), - mode: ['create'], deps: ['instance_series'], - type: (state) => { - return { - type: 'select', - options: state.instanceData, - optionsReloadBasis: state.instance_series, - controlProps: { - allowClear: false, - filter: (options) => { - if (options.length == 0) return; - let _types = _.filter(options, {'familyName': state.instance_series}), - _options = []; - _.forEach(_types, (value) => { - _options.push({ - 'label': value.instanceTypeName + ' (' + value.cpu + 'vCPU, ' + value.ram + 'GB RAM)', - 'value': value.instanceTypeName + ' (' + value.cpu + 'vCPU, ' + value.ram + 'GB RAM)' + '||' + value.instanceTypeId, - }); - }); - return _options; - }, - } - }; - }, noEmpty: true, - }, - ]; - } -} - - -class BigAnimalVolumeSchema extends BaseUISchema { - constructor(fieldOptions = {}, initValues = {}) { - super({ - oid: undefined, - volume_type: '', - volume_properties: '', - ...initValues - }); - - this.fieldOptions = { - ...fieldOptions, - }; - this.initValues = initValues; - } - - get idAttribute() { - return 'oid'; - } - - get baseFields() { - return [ - { - id: 'volume_type', label: gettext('Volume type'), - mode: ['create'], deps: [['region']], - type: (state) => { - return { - type: 'select', - options: ()=>this.fieldOptions.volume_types(state.region), - optionsReloadBasis: state.region, - }; - }, noEmpty: true, - },{ - id: 'volume_properties', label: gettext('Volume properties'), - mode: ['create'], deps: ['volume_type'], - type: (state) => { - return { - type: 'select', - options: ()=>this.fieldOptions.volume_properties(state.region, state.volume_type), - optionsReloadBasis: state.volume_type, - }; - }, noEmpty: true, - }, - ]; - } -} - -class BigAnimalDatabaseSchema extends BaseUISchema { - constructor(fieldOptions = {}, initValues = {}) { - super({ - oid: undefined, - password: '', - confirm_password: '', - database_type: '', - postgres_version: '', - high_availability: false, - replicas: 0, - ...initValues - }); - - this.fieldOptions = { - ...fieldOptions, - }; - this.initValues = initValues; - } - - - validate(data, setErrMsg) { - if(!isEmptyString(data.password) && !isEmptyString(data.confirm_password) - && data.password != data.confirm_password) { - setErrMsg('confirm_password', gettext('Passwords do not match.')); - return true; - } - if (!isEmptyString(data.confirm_password) && data.confirm_password.length < 12) { - setErrMsg('confirm_password', gettext('Password must be 12 characters or more.')); - return true; - } - if (data.high_availability && (isEmptyString(data.replicas) || data.replicas <= 0)) { - setErrMsg('replicas', gettext('Please select number of stand by replicas.')); - return true; - } - return false; - } - - get idAttribute() { - return 'oid'; - } - - get baseFields() { - return [ - { - id: 'gid', label: gettext('pgAdmin server group'), type: 'select', - options: this.fieldOptions.server_groups, - mode: ['create'], - controlProps: { allowClear: false }, - noEmpty: true, - }, { - id: 'database_type', label: gettext('Database type'), mode: ['create'], - type: 'select', - options: this.fieldOptions.db_types, - noEmpty: true, orientation: 'vertical', - },{ - id: 'postgres_version', label: gettext('PostgreSQL version'), - mode: ['create'], noEmpty: true, deps: ['database_type'], - options: this.fieldOptions.db_versions, - type: (state) => { - return { - type: 'select', - options: ()=>this.fieldOptions.db_versions(state.database_type), - optionsReloadBasis: state.database_type, - }; - }, - },{ - id: 'password', label: gettext('Database password'), type: 'password', - mode: ['create'], noEmpty: true, - },{ - id: 'confirm_password', label: gettext('Confirm password'), type: 'password', - mode: ['create'], noEmpty: true, - },{ - type: 'nested-fieldset', label: gettext('Availability'), - mode: ['create'], - schema: new BigAnimalHighAvailSchema(), - }, - - ]; - } -} - -class BigAnimalClusterSchema extends BaseUISchema { - constructor(fieldOptions = {}, initValues = {}) { - super({ - oid: undefined, - name: '', - region: '', - cloud_type: 'public', - biganimal_public_ip: initValues.hostIP, - ...initValues - }); - - this.fieldOptions = { - ...fieldOptions, - }; - this.initValues = initValues; - - this.instance_types = new BigAnimalInstanceSchema({ - instance_types: this.fieldOptions.instance_types, - }); - this.volume_types = new BigAnimalVolumeSchema({ - volume_types: this.fieldOptions.volume_types, - volume_properties: this.fieldOptions.volume_properties - }); - } - - get idAttribute() { - return 'oid'; - } - - get baseFields() { - return [ - { - id: 'name', label: gettext('Cluster name'), type: 'text', - mode: ['create'], noEmpty: true, - },{ - id: 'region', label: gettext('Region'), type: 'select', - options: this.fieldOptions.regions, - controlProps: { allowClear: false }, - noEmpty: true, - mode: ['create'], - },{ - id: 'biganimal_public_ip', label: gettext('Public IP range'), type: 'text', - mode: ['create'], - helpMessage: gettext('IP address range for allowed inbound traffic, for example: 127.0.0.1/32. Add multiple IP addresses/ranges separated with commas. Leave blank for 0.0.0.0/0'), - },{ - type: 'nested-fieldset', label: gettext('Instance Type'), - mode: ['create'], deps: ['region'], - schema: this.instance_types, - },{ - type: 'nested-fieldset', label: gettext('Storage'), - mode: ['create'], deps: ['region'], - schema: this.volume_types, - } - ]; - } -} - - - -class BigAnimalHighAvailSchema extends BaseUISchema { - constructor(fieldOptions = {}, initValues = {}) { - super({ - oid: undefined, - high_availability: false, - replicas: 0, - ...initValues - }); - - this.fieldOptions = { - ...fieldOptions, - }; - this.initValues = initValues; - } - - get idAttribute() { - return 'oid'; - } - - get baseFields() { - return [ - { - id: 'high_availability_note', type: 'note', - mode: ['create'], - text: gettext('High availability clusters are configured with one primary and up to two ' - + 'standby replicas. Clusters are configured across availability zones in regions with availability zones.'), - },{ - id: 'high_availability', label: gettext('High availability'), type: 'switch', - mode: ['create'] - },{ - id: 'replicas', label: gettext('Number of standby replicas'), type: 'select', - mode: ['create'], deps: ['high_availability'], - controlProps: { allowClear: false }, - helpMessage: gettext('Adding standby replicas will increase your number of CPUs, as well as your cost.'), - options: [ - {'label': gettext('1'), 'value': 1}, - {'label': gettext('2'), 'value': 2}, - ], noEmpty: true, - disabled: (state) => { - return !state.high_availability; - } - }, - ]; - } -} - -export { - CloudInstanceDetailsSchema, - CloudDBCredSchema, - DatabaseSchema, - BigAnimalClusterSchema, - BigAnimalDatabaseSchema -}; diff --git a/web/pgadmin/static/img/ms_azure.svg b/web/pgadmin/static/img/ms_azure.svg new file mode 100644 index 000000000..ae0a51d80 --- /dev/null +++ b/web/pgadmin/static/img/ms_azure.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + diff --git a/web/pgadmin/static/js/components/ExternalIcon.jsx b/web/pgadmin/static/js/components/ExternalIcon.jsx index 94d0b4603..5f7b94950 100644 --- a/web/pgadmin/static/js/components/ExternalIcon.jsx +++ b/web/pgadmin/static/js/components/ExternalIcon.jsx @@ -19,6 +19,7 @@ import BigAnimal from '../../img/biganimal.svg?svgr'; import Azure from '../../img/azure.svg?svgr'; import SQLFileSvg from '../../img/sql_file.svg?svgr'; import MagicSvg from '../../img/magic.svg?svgr'; +import MsAzure from '../../img/ms_azure.svg?svgr'; export default function ExternalIcon({Icon, ...props}) { return ; @@ -84,3 +85,6 @@ SQLFileIcon.propTypes = {style: PropTypes.object}; export const MagicIcon = ({style})=>; MagicIcon.propTypes = {style: PropTypes.object}; + +export const MSAzureIcon = ({style})=>; +MSAzureIcon.propTypes = {style: PropTypes.object}; diff --git a/web/pgadmin/static/js/helpers/wizard/Wizard.jsx b/web/pgadmin/static/js/helpers/wizard/Wizard.jsx index 92ffb20d9..462a16f30 100644 --- a/web/pgadmin/static/js/helpers/wizard/Wizard.jsx +++ b/web/pgadmin/static/js/helpers/wizard/Wizard.jsx @@ -139,8 +139,12 @@ function Wizard({ stepList, onStepChange, onSave, className, ...props }) { const handleNext = () => { // beforeNext should always return a promise if(props.beforeNext) { - props.beforeNext(activeStep).then(()=>{ - setActiveStep((prevActiveStep) => prevActiveStep + 1); + props.beforeNext(activeStep).then((skipCurrentStep=false)=>{ + if (skipCurrentStep) { + setActiveStep((prevActiveStep) => prevActiveStep + 2); + } else { + setActiveStep((prevActiveStep) => prevActiveStep + 1); + } }).catch(()=>{/*This is intentional (SonarQube)*/}); } else { setActiveStep((prevActiveStep) => prevActiveStep + 1); @@ -150,8 +154,12 @@ function Wizard({ stepList, onStepChange, onSave, className, ...props }) { const handleBack = () => { // beforeBack should always return a promise if(props.beforeBack) { - props.beforeBack(activeStep).then(()=>{ - setActiveStep((prevActiveStep) => prevActiveStep - 1 < 0 ? prevActiveStep : prevActiveStep - 1); + props.beforeBack(activeStep).then((skipCurrentStep=false)=>{ + if (skipCurrentStep) { + setActiveStep((prevActiveStep) => prevActiveStep - 1 < 0 ? prevActiveStep : prevActiveStep - 2); + } else { + setActiveStep((prevActiveStep) => prevActiveStep - 1 < 0 ? prevActiveStep : prevActiveStep - 1); + } }).catch(()=>{/*This is intentional (SonarQube)*/}); } else { setActiveStep((prevActiveStep) => prevActiveStep - 1 < 0 ? prevActiveStep : prevActiveStep - 1);