pgadmin4/web/pgadmin/misc/cloud/static/js/CloudWizard.jsx
Khushboo Vashi ffc1c6c3b7 Fixed following issues related to cloud deployment:
1) The Mumbai region issue has been resolved
2) Display name of regions has been modified appropriately
3) The password field has been validated the same way as AWS
4) Added support for a list of IP addresses in the public IP address range field.
2022-02-28 18:19:18 +05:30

470 lines
15 KiB
JavaScript

/////////////////////////////////////////////////////////////
//
// 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 url_for from 'sources/url_for';
import React from 'react';
import { Box, Table, TableBody, TableCell, TableHead, TableRow, Paper } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import Wizard from '../../../../static/js/helpers/wizard/Wizard';
import WizardStep from '../../../../static/js/helpers/wizard/WizardStep';
import {FormFooterMessage, MESSAGE_TYPE, InputToggle } from '../../../../static/js/components/FormComponents';
import getApiInstance from '../../../../static/js/api_instance';
import SchemaView from '../../../../static/js/SchemaView';
import Alertify from 'pgadmin.alertifyjs';
import PropTypes from 'prop-types';
import {CloudInstanceDetailsSchema, CloudDBCredSchema, DatabaseSchema} from './cloud_db_details_schema.ui';
import { isEmptyString } from 'sources/validators';
import pgAdmin from 'sources/pgadmin';
import { getNodeAjaxOptions, getNodeListById } from 'pgbrowser/node_ajax';
import { commonTableStyles } from '../../../../static/js/Theme';
import clsx from 'clsx';
const useStyles = makeStyles(() =>
({
messageBox: {
marginBottom: '1em',
display: 'flex',
},
messagePadding: {
flex: 2.5
},
toggleButton: {
height: '100px',
},
table: {
marginLeft: '4px',
marginTop: '12px',
},
tableCellHeading: {
fontWeight: 'bold',
paddingLeft: '9px',
},
tableCell: {
padding: '9px',
paddingLeft: '11px',
}
}),
);
export default function CloudWizard({ nodeInfo, nodeData }) {
const classes = useStyles();
const tableClasses = commonTableStyles();
var steps = ['Cloud Provider', 'Credentials', 'Instance Specification', 'Database Details', 'Review'];
const [currentStep, setCurrentStep] = React.useState('');
const [selectionVal, setCloudSelection] = React.useState('');
const [errMsg, setErrMsg] = React.useState('');
const [cloudInstanceDetailsInstance, setCloudInstanceDetailsInstance] = React.useState();
const [cloudDBCredInstance, setCloudDBCredInstance] = React.useState();
const [cloudDBInstance, setCloudDBInstance] = React.useState();
const [cloudInstanceDetails, setCloudInstanceDetails] = React.useState({});
const [cloudDBCred, setCloudDBCred] = React.useState({});
const [cloudDBDetails, setCloudDBDetails] = React.useState({});
const [callRDSAPI, setCallRDSAPI] = React.useState({});
const [hostIP, setHostIP] = React.useState('127.0.0.1/32');
const axiosApi = getApiInstance();
React.useEffect(() => {
let _url = url_for('cloud.get_host_ip') ;
axiosApi.get(_url)
.then((res) => {
if (res.data.data) {
setHostIP(res.data.data);
}
})
.catch((error) => {
Alertify.error(gettext(`Error while getting the host ip: ${error.response.data.errormsg}`));
});
}, []);
React.useEffect(() => {
if (callRDSAPI == 2) {
const cloudDBInstanceSchema = new CloudInstanceDetailsSchema({
version: ()=>getNodeAjaxOptions('get_aws_db_versions', pgAdmin.Browser.Nodes['server'], nodeInfo, nodeData, {
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('cloud.get_aws_db_versions');
}
}),
getInstances: (engine, reload, options) =>
{
return new Promise((resolve, reject)=>{
const api = getApiInstance();
var _url = url_for('cloud.get_aws_db_instances') ;
if (engine) _url += '?eng_version=' + engine;
if (reload || options === undefined || options.length == 0) {
api.get(_url)
.then(res=>{
let data = res.data.data;
resolve(data);
})
.catch((err)=>{
reject(err);
});
} else {
resolve(options);
}
});
},
instance_type: ()=>getNodeAjaxOptions('get_aws_db_instances', pgAdmin.Browser.Nodes['server'], nodeInfo, nodeData, {
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('cloud.get_aws_db_instances');
}
}),
server_groups: ()=>getNodeListById(pgAdmin.Browser.Nodes['server_group'], nodeInfo, nodeData),
}, {
gid: nodeInfo['server_group']._id,
hostIP: hostIP,
});
setCloudInstanceDetailsInstance(cloudDBInstanceSchema);
}
}, [callRDSAPI]);
React.useEffect(() => {
const cloudDBCredSchema = new CloudDBCredSchema({
regions: ()=>getNodeAjaxOptions('get_aws_regions', pgAdmin.Browser.Nodes['server'], nodeInfo, nodeData, {
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('cloud.get_aws_regions');
}
}),
});
setCloudDBCredInstance(cloudDBCredSchema);
const cloudDBSchema = new DatabaseSchema({
server_groups: ()=>getNodeListById(pgAdmin.Browser.Nodes['server_group'], nodeInfo, nodeData),
},
{
gid: nodeInfo['server_group']._id,
}
);
setCloudDBInstance(cloudDBSchema);
}, []);
const wizardStepChange = (data) => {
setCurrentStep(data.currentStep);
};
const validateCloudStep1 = (cloudDBCred) => {
let isError = false;
if (isEmptyString(cloudDBCred.aws_access_key) || isEmptyString(cloudDBCred.aws_secret_access_key)) {
isError = true;
}
return isError;
};
const validateCloudStep2 = (cloudInstanceDetails, host_ip) => {
let isError = false;
if (isEmptyString(cloudInstanceDetails.aws_name) ||
isEmptyString(cloudInstanceDetails.aws_db_version) || isEmptyString(cloudInstanceDetails.aws_instance_type) ||
isEmptyString(cloudInstanceDetails.aws_storage_type)|| isEmptyString(cloudInstanceDetails.aws_storage_size)) {
isError = true;
}
if(cloudInstanceDetails.aws_storage_type == 'io1' && isEmptyString(cloudInstanceDetails.aws_storage_IOPS)) {
isError = true;
}
if (isEmptyString(cloudInstanceDetails.aws_public_ip)) cloudInstanceDetails.aws_public_ip = host_ip;
return isError;
};
const validateCloudStep3 = (cloudDBDetails) => {
let isError = false;
if (isEmptyString(cloudDBDetails.aws_db_name) ||
isEmptyString(cloudDBDetails.aws_db_username) || isEmptyString(cloudDBDetails.aws_db_password)) {
isError = true;
}
if (isEmptyString(cloudDBDetails.aws_db_port)) cloudDBDetails.aws_db_port = 5432;
if (isEmptyString(cloudDBDetails.gid)) cloudDBDetails.gid = nodeInfo['server_group']._id;
return isError;
};
const getStorageType = (cloudInstanceDetails) => {
let _storage_type = 'General Purpose SSD (gp2)',
_io1 = undefined;
if(cloudInstanceDetails.aws_storage_type == 'gp2') _storage_type = 'General Purpose SSD (gp2)';
else if(cloudInstanceDetails.aws_storage_type == 'io1') {
_storage_type = 'Provisioned IOPS SSD (io1)';
_io1 = cloudInstanceDetails.aws_storage_IOPS;
}
else if(cloudInstanceDetails.aws_storage_type == 'magnetic') _storage_type = 'Magnetic';
return [_io1, _storage_type];
};
const onSave = () => {
var _url = url_for('cloud.deploy_on_cloud');
const post_data = {
gid: nodeInfo.server_group._id,
cloud: selectionVal,
secret: cloudDBCred,
instance_details:cloudInstanceDetails,
db_details: cloudDBDetails
};
axiosApi.post(_url, post_data)
.then((res) => {
pgAdmin.Browser.Events.trigger('pgadmin:browser:tree:add', res.data.data.node, {'server_group': nodeInfo['server_group']});
pgAdmin.Browser.Events.trigger('pgadmin-bgprocess:created', Alertify.cloudWizardDialog());
Alertify.cloudWizardDialog().close();
})
.catch((error) => {
Alertify.error(gettext(`Error while saving cloud wizard data: ${error.response.data.errormsg}`));
});
};
const disableNextCheck = () => {
setCallRDSAPI(currentStep);
let isError = false;
switch (currentStep) {
case 0:
setCloudSelection('rds');
break;
case 1:
isError = validateCloudStep1(cloudDBCred);
break;
case 2:
isError = validateCloudStep2(cloudInstanceDetails, hostIP);
break;
case 3:
isError = validateCloudStep3(cloudDBDetails);
break;
default:
break;
}
return isError;
};
const onBeforeNext = (activeStep) => {
return new Promise((resolve, reject)=>{
if(activeStep == 1) {
setErrMsg([MESSAGE_TYPE.INFO, 'Validating credentials...']);
var _url = url_for('cloud.verify_credentials');
const post_data = {
cloud: selectionVal,
secret: cloudDBCred,
};
axiosApi.post(_url, post_data)
.then((res) => {
if(!res.data.success) {
setErrMsg([MESSAGE_TYPE.ERROR, res.data.info]);
reject();
} else {
setErrMsg(['', '']);
resolve();
}
})
.catch(() => {
setErrMsg([MESSAGE_TYPE.ERROR, 'Error while checking cloud credentials']);
reject();
});
} else {
resolve();
}
});
};
const onDialogHelp = () => {
window.open(url_for('help.static', { 'filename': 'cloud_deployment.html' }), 'pgadmin_help');
};
function createData(name, value) {
return { name, value };
}
let cloud = '';
switch (selectionVal) {
case 'rds':
cloud = 'Amazon RDS';
break;
case 'azure':
cloud = 'Azure PostgreSQL';
break;
case 'biganimal':
cloud = 'EDB Big Animal';
break;
}
const rows1 = [
createData('Cloud', cloud),
createData('Instance name', cloudInstanceDetails.aws_name),
createData('Public IP', cloudInstanceDetails.aws_public_ip),
];
const rows2 = [
createData('PostgreSQL version', cloudInstanceDetails.aws_db_version),
createData('Instance type', cloudInstanceDetails.aws_instance_type),
];
let _storage_type = getStorageType(cloudInstanceDetails);
const rows3 = [
createData('Storage type', _storage_type[1]),
createData('Allocated storage', cloudInstanceDetails.aws_storage_size + ' GiB'),
];
if (_storage_type[0] !== undefined) {
rows3.push(createData('Provisioned IOPS', _storage_type[0]));
}
const rows4 = [
createData('Database name', cloudDBDetails.aws_db_name),
createData('Username', cloudDBDetails.aws_db_username),
createData('Password', 'xxxxxxx'),
createData('Port', cloudDBDetails.aws_db_port),
];
const onErrClose = React.useCallback(()=>{
setErrMsg([]);
});
const displayTableRows = (rows) => {
return rows.map((row) => (
<TableRow key={row.name} >
<TableCell scope="row">{row.name}</TableCell>
<TableCell align="right">{row.value}</TableCell>
</TableRow>
));
};
return (
<>
<Wizard
title={gettext('Deploy Cloud Instance')}
stepList={steps}
disableNextStep={disableNextCheck}
onStepChange={wizardStepChange}
onSave={onSave}
onHelp={onDialogHelp}
beforeNext={onBeforeNext}>
<WizardStep stepId={0}>
<Box className={classes.messageBox}>
<Box className={classes.messagePadding}>{gettext('Deploy on Amazon RDS cloud.')}</Box>
</Box>
<Box className={classes.messageBox}>
<InputToggle
value='rds'
options={[{'label': gettext('Amazon RDS'), value: 'rds'}]}
className={classes.toggleButton}
onChange={(value) => {
setCloudSelection(value);}
}
>
</InputToggle>
</Box>
<Box className={classes.messageBox}>
<Box className={classes.messagePadding}>{gettext('More cloud providers are coming soon...')}</Box>
</Box>
</WizardStep>
<WizardStep stepId={1} >
{cloudDBCredInstance &&
<SchemaView
formType={'dialog'}
getInitData={() => { /*This is intentional (SonarQube)*/ }}
viewHelperProps={{ mode: 'create' }}
schema={cloudDBCredInstance}
showFooter={false}
isTabView={false}
onDataChange={(isChanged, changedData) => {
setCloudDBCred(changedData);
}}
/>
}
<FormFooterMessage type={errMsg[0]} message={errMsg[1]} onClose={onErrClose} />
</WizardStep>
<WizardStep stepId={2} >
{cloudInstanceDetailsInstance &&
<SchemaView
formType={'dialog'}
getInitData={() => { /*This is intentional (SonarQube)*/ }}
viewHelperProps={{ mode: 'create' }}
schema={cloudInstanceDetailsInstance}
showFooter={false}
isTabView={false}
onDataChange={(isChanged, changedData) => {
setCloudInstanceDetails(changedData);
}}
/>
}
</WizardStep>
<WizardStep stepId={3} >
{cloudDBInstance &&
<SchemaView
formType={'dialog'}
getInitData={() => { /*This is intentional (SonarQube)*/ }}
viewHelperProps={{ mode: 'create' }}
schema={cloudDBInstance}
showFooter={false}
isTabView={false}
onDataChange={(isChanged, changedData) => {
setCloudDBDetails(changedData);
}}
/>
}
</WizardStep>
<WizardStep stepId={4} >
<Box className={classes.boxText}>{gettext('Please review the details before creating the cloud instance.')}</Box>
<Paper variant="outlined" elevation={0} className={classes.summaryContainer}>
<Table aria-label="simple table" className={clsx(tableClasses.table)}>
<TableBody>
{displayTableRows(rows1)}
</TableBody>
</Table>
<Table aria-label="simple table" className={clsx(tableClasses.table)}>
<TableHead>
<TableRow>
<TableCell colSpan={2}>{gettext('Version and Instance Details')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{displayTableRows(rows2)}
</TableBody>
</Table>
<Table aria-label="simple table" className={clsx(tableClasses.table)}>
<TableHead>
<TableRow>
<TableCell colSpan={2}>{gettext('Storage Details')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{displayTableRows(rows3)}
</TableBody>
</Table>
<Table aria-label="simple table" className={clsx(tableClasses.table)}>
<TableHead>
<TableRow>
<TableCell colSpan={2}>{gettext('Database Details')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{displayTableRows(rows4)}
</TableBody>
</Table>
</Paper>
</WizardStep>
</Wizard>
</>
);
}
CloudWizard.propTypes = {
nodeInfo: PropTypes.object,
nodeData: PropTypes.object,
};