Added copy server support, allowing the duplication of existing servers with the option to make certain modifications. #6085 (#7106)
Added copy server support, allowing the duplication of existing servers with the option to make certain modifications. #6085pull/7109/head
parent
5e710f7ee3
commit
30509d1bc1
Binary file not shown.
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 20 KiB |
Binary file not shown.
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 112 KiB |
Binary file not shown.
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 65 KiB |
|
@ -42,6 +42,12 @@ following options (in alphabetical order):
|
|||
+-----------------------------+--------------------------------------------------------------------------------------------------------------------------+
|
||||
| Option | Action |
|
||||
+=============================+==========================================================================================================================+
|
||||
| *Register* | |
|
||||
| | |
|
||||
| 1) *Server* | Click to open the :ref:`Server <server_dialog>` dialog to register a server. |
|
||||
| | |
|
||||
| 2) *Deploy Cloud Instance*| Click to open the :ref:`Cloud Deployment <cloud_deployment>` dialog to deploy an cloud instance. |
|
||||
+-----------------------------+--------------------------------------------------------------------------------------------------------------------------+
|
||||
| *Change Password...* | Click to open the :ref:`Change Password... <change_password_dialog>` dialog to change your password. |
|
||||
+-----------------------------+--------------------------------------------------------------------------------------------------------------------------+
|
||||
| *Clear Saved Password* | If you have saved the database server password, click to clear the saved password. |
|
||||
|
@ -52,6 +58,8 @@ following options (in alphabetical order):
|
|||
+-----------------------------+--------------------------------------------------------------------------------------------------------------------------+
|
||||
| *Connect Server* | Click to open the :ref:`Connect to Server <connect_to_server>` dialog to establish a connection with a server. |
|
||||
+-----------------------------+--------------------------------------------------------------------------------------------------------------------------+
|
||||
| *Copy Server...* | Click to copy the currently selected server. |
|
||||
+-----------------------------+--------------------------------------------------------------------------------------------------------------------------+
|
||||
| *Create* | Click *Create* to access a context menu that provides context-sensitive selections. |
|
||||
| | Your selection opens a *Create* dialog for creating a new object. |
|
||||
+-----------------------------+--------------------------------------------------------------------------------------------------------------------------+
|
||||
|
|
|
@ -22,6 +22,7 @@ New features
|
|||
|
||||
| `Issue #2483 <https://github.com/pgadmin-org/pgadmin4/issues/2483>`_ - Administer pgAdmin Users and Preferences Using the Command Line Interface (CLI).
|
||||
| `Issue #5908 <https://github.com/pgadmin-org/pgadmin4/issues/5908>`_ - Allow users to convert View/Edit table into a Query tool to enable editing the SQL generated.
|
||||
| `Issue #6085 <https://github.com/pgadmin-org/pgadmin4/issues/6085>`_ - Added copy server support, allowing the duplication of existing servers with the option to make certain modifications.
|
||||
| `Issue #7016 <https://github.com/pgadmin-org/pgadmin4/issues/7016>`_ - Added keep-alive support for SSH sessions when connecting to a PostgreSQL server via an SSH tunnel.
|
||||
|
||||
Housekeeping
|
||||
|
|
|
@ -39,8 +39,9 @@ def get_icon_css_class(group_id, group_user_id,
|
|||
group_user_id != current_user.id and
|
||||
ServerGroupModule.has_shared_server(group_id)):
|
||||
default_val = 'icon-server_group_shared'
|
||||
return default_val, True
|
||||
|
||||
return default_val
|
||||
return default_val, False
|
||||
|
||||
|
||||
SG_NOT_FOUND_ERROR = 'The specified server group could not be found.'
|
||||
|
@ -86,14 +87,16 @@ class ServerGroupModule(BrowserPluginModule):
|
|||
).order_by("id")
|
||||
|
||||
for idx, group in enumerate(groups):
|
||||
icon_class, is_shared = get_icon_css_class(group.id, group.user_id)
|
||||
yield self.generate_browser_node(
|
||||
"%d" % (group.id), None,
|
||||
group.name,
|
||||
get_icon_css_class(group.id, group.user_id),
|
||||
icon_class,
|
||||
True,
|
||||
self.node_type,
|
||||
can_delete=True if idx > 0 else False,
|
||||
user_id=group.user_id
|
||||
user_id=group.user_id,
|
||||
is_shared=is_shared
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -264,15 +267,17 @@ class ServerGroupView(NodeView):
|
|||
status=410, success=0, errormsg=e.message
|
||||
)
|
||||
|
||||
icon_class, is_shared = get_icon_css_class(gid, servergroup.user_id)
|
||||
return jsonify(
|
||||
node=self.blueprint.generate_browser_node(
|
||||
gid,
|
||||
None,
|
||||
servergroup.name,
|
||||
get_icon_css_class(gid, servergroup.user_id),
|
||||
icon_class,
|
||||
True,
|
||||
self.node_type,
|
||||
can_delete=True # This is user created hence can deleted
|
||||
can_delete=True, # This is user created hence can delete
|
||||
is_shared=is_shared
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -311,16 +316,18 @@ class ServerGroupView(NodeView):
|
|||
data['id'] = sg.id
|
||||
data['name'] = sg.name
|
||||
|
||||
icon_class, is_shared = get_icon_css_class(sg.id, sg.user_id)
|
||||
return jsonify(
|
||||
node=self.blueprint.generate_browser_node(
|
||||
"%d" % sg.id,
|
||||
None,
|
||||
sg.name,
|
||||
get_icon_css_class(sg.id, sg.user_id),
|
||||
icon_class,
|
||||
True,
|
||||
self.node_type,
|
||||
# This is user created hence can deleted
|
||||
can_delete=True
|
||||
can_delete=True,
|
||||
is_shared=is_shared
|
||||
)
|
||||
)
|
||||
except exc.IntegrityError:
|
||||
|
@ -399,14 +406,17 @@ class ServerGroupView(NodeView):
|
|||
groups = ServerGroup.query.filter_by(user_id=current_user.id)
|
||||
|
||||
for group in groups:
|
||||
icon_class, is_shared = get_icon_css_class(group.id,
|
||||
group.user_id)
|
||||
nodes.append(
|
||||
self.blueprint.generate_browser_node(
|
||||
"%d" % group.id,
|
||||
None,
|
||||
group.name,
|
||||
get_icon_css_class(group.id, group.user_id),
|
||||
icon_class,
|
||||
True,
|
||||
self.node_type
|
||||
self.node_type,
|
||||
is_shared=is_shared
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
@ -417,12 +427,15 @@ class ServerGroupView(NodeView):
|
|||
errormsg=gettext("Could not find the server group.")
|
||||
)
|
||||
|
||||
icon_class, is_shared = get_icon_css_class(group.id,
|
||||
group.user_id)
|
||||
nodes = self.blueprint.generate_browser_node(
|
||||
"%d" % (group.id), None,
|
||||
group.name,
|
||||
get_icon_css_class(group.id, group.user_id),
|
||||
icon_class,
|
||||
True,
|
||||
self.node_type
|
||||
self.node_type,
|
||||
is_shared=is_shared
|
||||
)
|
||||
|
||||
return make_json_response(data=nodes)
|
||||
|
|
|
@ -1201,7 +1201,7 @@ class ServerNode(PGChildNodeView):
|
|||
)
|
||||
|
||||
# To check ssl configuration
|
||||
is_ssl, connection_params = self.check_ssl_fields(connection_params)
|
||||
_, connection_params = self.check_ssl_fields(connection_params)
|
||||
# set the connection params again in the data
|
||||
if 'connection_params' in data:
|
||||
data['connection_params'] = connection_params
|
||||
|
@ -1221,8 +1221,8 @@ class ServerNode(PGChildNodeView):
|
|||
config.ALLOW_SAVE_PASSWORD else 0,
|
||||
comment=data.get('comment', None),
|
||||
role=data.get('role', None),
|
||||
db_res=','.join(data['db_res'])
|
||||
if 'db_res' in data else None,
|
||||
db_res=','.join(data['db_res']) if 'db_res' in data and
|
||||
isinstance(data['db_res'], list) else None,
|
||||
bgcolor=data.get('bgcolor', None),
|
||||
fgcolor=data.get('fgcolor', None),
|
||||
service=data.get('service', None),
|
||||
|
@ -1763,7 +1763,7 @@ class ServerNode(PGChildNodeView):
|
|||
|
||||
if conn.connected():
|
||||
# Execute the command for reload configuration for the server
|
||||
status, rid = conn.execute_scalar("SELECT pg_reload_conf();")
|
||||
status, _ = conn.execute_scalar("SELECT pg_reload_conf();")
|
||||
|
||||
if not status:
|
||||
return internal_server_error(
|
||||
|
@ -1782,7 +1782,7 @@ class ServerNode(PGChildNodeView):
|
|||
|
||||
def create_restore_point(self, gid, sid):
|
||||
"""
|
||||
This method will creates named restore point
|
||||
This method will create named restore point
|
||||
|
||||
Args:
|
||||
gid: Server group ID
|
||||
|
|
|
@ -44,9 +44,28 @@ define('pgadmin.node.server', [
|
|||
title: function(d, action) {
|
||||
if(action == 'create') {
|
||||
return gettext('Register - %s', this.label);
|
||||
} else if (action == 'copy') {
|
||||
return gettext('Copy Server - %s', d.label);
|
||||
}
|
||||
return d._label??'';
|
||||
},
|
||||
copy: function(d) {
|
||||
// This function serves the purpose of facilitating modifications
|
||||
// during the server copying process.
|
||||
|
||||
// Changing the name of the server to "Copy of <existing name>"
|
||||
d.name = gettext('Copy of %s', d.name);
|
||||
// If existing server is a shared server from another user then
|
||||
// copy this server as a local server for the current user.
|
||||
if (d?.shared && d.user_id != current_user?.id) {
|
||||
d.gid = null;
|
||||
d.user_id = current_user?.id;
|
||||
d.shared = false;
|
||||
d.server_owner = null;
|
||||
d.shared_username = null;
|
||||
}
|
||||
return d;
|
||||
},
|
||||
Init: function() {
|
||||
/* Avoid multiple registration of same menus */
|
||||
if (this.initialized)
|
||||
|
@ -135,6 +154,11 @@ define('pgadmin.node.server', [
|
|||
data: {
|
||||
data_disabled: gettext('SSH Tunnel password is not saved for selected server.'),
|
||||
},
|
||||
}, {
|
||||
name: 'copy_server', node: 'server', module: this,
|
||||
applies: ['object', 'context'], callback: 'show_obj_properties',
|
||||
label: gettext('Copy Server...'), data: {action: 'copy'},
|
||||
priority: 4,
|
||||
}]);
|
||||
|
||||
_.bindAll(this, 'connection_lost');
|
||||
|
@ -501,7 +525,9 @@ define('pgadmin.node.server', [
|
|||
},
|
||||
getSchema: (treeNodeInfo, itemNodeData)=>{
|
||||
return new ServerSchema(
|
||||
getNodeListById(pgBrowser.Nodes['server_group'], treeNodeInfo, itemNodeData),
|
||||
getNodeListById(pgBrowser.Nodes['server_group'], treeNodeInfo, itemNodeData, {},
|
||||
// Filter out shared servers group, it should not be visible.
|
||||
(server)=> !server.is_shared),
|
||||
itemNodeData.user_id,
|
||||
{
|
||||
gid: treeNodeInfo['server_group']._id,
|
||||
|
|
|
@ -370,6 +370,14 @@ export default class ServerSchema extends BaseUISchema {
|
|||
validate(state, setError) {
|
||||
let errmsg = null;
|
||||
|
||||
if(isEmptyString(state.gid)) {
|
||||
errmsg = gettext('Server group must be specified.');
|
||||
setError('gid', errmsg);
|
||||
return true;
|
||||
} else {
|
||||
setError('gid', null);
|
||||
}
|
||||
|
||||
if (isEmptyString(state.service)) {
|
||||
errmsg = gettext('Either Host name or Service must be specified.');
|
||||
if(isEmptyString(state.host)) {
|
||||
|
|
|
@ -95,6 +95,11 @@ define('pgadmin.browser.node', [
|
|||
}
|
||||
return d._label??'';
|
||||
},
|
||||
copy: function(d) {
|
||||
// This function serves the purpose of facilitating modifications
|
||||
// during the copying process of any node.
|
||||
return d;
|
||||
},
|
||||
hasId: true,
|
||||
///////
|
||||
// Initialization function
|
||||
|
@ -407,6 +412,36 @@ define('pgadmin.browser.node', [
|
|||
onSave: onSave,
|
||||
onClose: onClose,
|
||||
});
|
||||
} else if (args.action == 'copy') {
|
||||
// This else-if block is used to copy the existing object and
|
||||
// open the respective dialog. Add the copied object into the object
|
||||
// browser tree upon the 'Save' button click.
|
||||
treeNodeInfo = pgBrowser.tree.getTreeNodeHierarchy(nodeItem);
|
||||
const panelId = _.uniqueId(BROWSER_PANELS.EDIT_PROPERTIES);
|
||||
const onClose = (force=false)=>pgBrowser.docker.close(panelId, force);
|
||||
const onSave = (newNodeData)=>{
|
||||
// Clear the cache for this node now.
|
||||
setTimeout(()=>{
|
||||
this.clear_cache.apply(this, item);
|
||||
}, 0);
|
||||
try {
|
||||
pgBrowser.Events.trigger(
|
||||
'pgadmin:browser:tree:add', _.clone(newNodeData.node),
|
||||
{'server_group': treeNodeInfo['server_group']}
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn(e.stack || e);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
this.showPropertiesDialog(panelId, panelTitle, {
|
||||
treeNodeInfo: treeNodeInfo,
|
||||
item: nodeItem,
|
||||
nodeData: nodeData,
|
||||
actionType: 'copy',
|
||||
onSave: onSave,
|
||||
onClose: onClose,
|
||||
});
|
||||
} else {
|
||||
const panelId = BROWSER_PANELS.EDIT_PROPERTIES+nodeData.id;
|
||||
const onClose = (force=false)=>pgBrowser.docker.close(panelId, force);
|
||||
|
|
|
@ -11,6 +11,7 @@ import CloudWizard from './CloudWizard';
|
|||
import getApiInstance from '../../../../static/js/api_instance';
|
||||
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import current_user from 'pgadmin.user_management.current_user';
|
||||
|
||||
// Cloud Wizard
|
||||
define('pgadmin.misc.cloud', [
|
||||
|
@ -43,7 +44,7 @@ define('pgadmin.misc.cloud', [
|
|||
priority: 15,
|
||||
label: gettext('Deploy Cloud Instance...'),
|
||||
icon: 'wcTabIcon icon-server',
|
||||
enable: true,
|
||||
enable: 'canCreate',
|
||||
data: {action: 'create'},
|
||||
category: 'register',
|
||||
node: 'server_group',
|
||||
|
@ -55,7 +56,7 @@ define('pgadmin.misc.cloud', [
|
|||
priority: 15,
|
||||
label: gettext('Deploy Cloud Instance...'),
|
||||
icon: 'wcTabIcon icon-server',
|
||||
enable: true,
|
||||
enable: 'canCreate',
|
||||
data: {action: 'create'},
|
||||
category: 'register',
|
||||
node: 'server',
|
||||
|
@ -64,6 +65,10 @@ define('pgadmin.misc.cloud', [
|
|||
pgBrowser.add_menus(menus);
|
||||
return this;
|
||||
},
|
||||
canCreate: function(node){
|
||||
let serverOwner = node.user_id;
|
||||
return (serverOwner == current_user.id || _.isUndefined(serverOwner));
|
||||
},
|
||||
|
||||
// Callback to draw Wizard Dialog
|
||||
start_cloud_wizard: function() {
|
||||
|
|
|
@ -27,17 +27,29 @@ export default function ObjectNodeProperties({panelId, node, treeNodeInfo, nodeD
|
|||
let serverInfo = treeNodeInfo && ('server' in treeNodeInfo) &&
|
||||
pgAdmin.Browser.serverInfo && pgAdmin.Browser.serverInfo[treeNodeInfo.server._id];
|
||||
let inCatalog = treeNodeInfo && ('catalog' in treeNodeInfo);
|
||||
let urlBase = generateNodeUrl.call(node, treeNodeInfo, actionType, nodeData, false, node.url_jump_after_node);
|
||||
let isActionTypeCopy = actionType == 'copy';
|
||||
// If the actionType is set to 'copy' it is necessary to retrieve the details
|
||||
// of the existing node. Therefore, specify the actionType as 'edit' to
|
||||
// facilitate this process.
|
||||
let urlBase = generateNodeUrl.call(node, treeNodeInfo, isActionTypeCopy ? 'edit' : actionType, nodeData, false, node.url_jump_after_node);
|
||||
const api = getApiInstance();
|
||||
// To check node data is updated or not
|
||||
const staleCounter = useRef(0);
|
||||
const url = (isNew)=>{
|
||||
return urlBase + (isNew ? '' : nodeData._id);
|
||||
};
|
||||
const isDirty = useRef(false); // usefull for warnings
|
||||
const isDirty = useRef(false); // useful for warnings
|
||||
let warnOnCloseFlag = true;
|
||||
const confirmOnCloseReset = usePreferences().getPreferencesForModule('browser').confirm_on_properties_close;
|
||||
let updatedData = ['table', 'partition'].includes(nodeType) && !_.isEmpty(nodeData.rows_cnt) ? {rows_cnt: nodeData.rows_cnt} : undefined;
|
||||
let schema = node.getSchema.call(node, treeNodeInfo, nodeData);
|
||||
|
||||
// We only have two actionTypes, 'create' and 'edit' to initiate the dialog,
|
||||
// so if isActionTypeCopy is true, we should revert back to "create" since
|
||||
// we are duplicating the node.
|
||||
if (isActionTypeCopy) {
|
||||
actionType = 'create';
|
||||
}
|
||||
|
||||
let onError = (err)=> {
|
||||
if(err.response){
|
||||
|
@ -51,7 +63,7 @@ export default function ObjectNodeProperties({panelId, node, treeNodeInfo, nodeD
|
|||
|
||||
/* Called when dialog is opened in edit mode, promise required */
|
||||
let initData = ()=>new Promise((resolve, reject)=>{
|
||||
if(actionType === 'create') {
|
||||
if(actionType === 'create' && !isActionTypeCopy) {
|
||||
resolve({});
|
||||
} else {
|
||||
// Do not call the API if tab is not active.
|
||||
|
@ -60,7 +72,13 @@ export default function ObjectNodeProperties({panelId, node, treeNodeInfo, nodeD
|
|||
}
|
||||
api.get(url(false))
|
||||
.then((res)=>{
|
||||
resolve(res.data);
|
||||
let data = res.data;
|
||||
if (isActionTypeCopy) {
|
||||
// Delete the idAttribute while copying the node.
|
||||
delete data[schema.idAttribute];
|
||||
data = node.copy(data);
|
||||
}
|
||||
resolve(data);
|
||||
})
|
||||
.catch((err)=>{
|
||||
pgAdmin.Browser.notifier.pgNotifier('error', err, gettext('Failed to fetch data'), function(msg) {
|
||||
|
@ -192,7 +210,6 @@ export default function ObjectNodeProperties({panelId, node, treeNodeInfo, nodeD
|
|||
inCatalog: inCatalog,
|
||||
};
|
||||
|
||||
let schema = node.getSchema.call(node, treeNodeInfo, nodeData);
|
||||
// Show/Hide security group for nodes under the catalog
|
||||
if('catalog' in treeNodeInfo
|
||||
&& formType !== 'tab') {
|
||||
|
|
|
@ -191,7 +191,7 @@ class Server(db.Model):
|
|||
tunnel_port = db.Column(
|
||||
db.Integer(),
|
||||
db.CheckConstraint('port <= 65534'),
|
||||
nullable=True)
|
||||
nullable=True, default=22)
|
||||
tunnel_username = db.Column(db.String(64), nullable=True)
|
||||
tunnel_authentication = db.Column(
|
||||
db.Integer(),
|
||||
|
@ -201,7 +201,7 @@ class Server(db.Model):
|
|||
)
|
||||
tunnel_identity_file = db.Column(db.String(64), nullable=True)
|
||||
tunnel_password = db.Column(PgAdminDbBinaryString())
|
||||
tunnel_keep_alive = db.Column(db.Integer(), nullable=True)
|
||||
tunnel_keep_alive = db.Column(db.Integer(), nullable=True, default=0)
|
||||
shared = db.Column(db.Boolean(), nullable=False)
|
||||
shared_username = db.Column(db.String(64), nullable=True)
|
||||
kerberos_conn = db.Column(db.Boolean(), nullable=False, default=0)
|
||||
|
|
|
@ -42,6 +42,10 @@ describe('ServerSchema', ()=>{
|
|||
let state = {};
|
||||
let setError = jest.fn();
|
||||
|
||||
schemaObj.validate(state, setError);
|
||||
expect(setError).toHaveBeenCalledWith('gid', 'Server group must be specified.');
|
||||
|
||||
state.gid = 1;
|
||||
schemaObj.validate(state, setError);
|
||||
expect(setError).toHaveBeenCalledWith('host', 'Either Host name or Service must be specified.');
|
||||
|
||||
|
|
Loading…
Reference in New Issue