Skip to content

Commit

Permalink
Added copy server support, allowing the duplication of existing serve…
Browse files Browse the repository at this point in the history
…rs with the option to make certain modifications. pgadmin-org#6085

Fixed review comment
  • Loading branch information
akshay-joshi committed Jan 8, 2024
1 parent 5e710f7 commit 90b9a86
Show file tree
Hide file tree
Showing 14 changed files with 143 additions and 26 deletions.
Binary file modified docs/en_US/images/file_menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/en_US/images/object_menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/en_US/images/runtime_menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions docs/en_US/menu_bar.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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. |
+-----------------------------+--------------------------------------------------------------------------------------------------------------------------+
Expand Down
1 change: 1 addition & 0 deletions docs/en_US/release_notes_8_2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 24 additions & 11 deletions web/pgadmin/browser/server_groups/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
Expand Down Expand Up @@ -86,14 +87,16 @@ def get_nodes(self, *arg, **kwargs):
).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
Expand Down Expand Up @@ -264,15 +267,17 @@ def update(self, gid):
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
)
)

Expand Down Expand Up @@ -311,16 +316,18 @@ def create(self):
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:
Expand Down Expand Up @@ -399,14 +406,17 @@ def nodes(self, gid=None):
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:
Expand All @@ -417,12 +427,15 @@ def nodes(self, gid=None):
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)
Expand Down
10 changes: 5 additions & 5 deletions web/pgadmin/browser/server_groups/servers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1201,7 +1201,7 @@ def create(self, gid):
)

# 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
Expand All @@ -1221,8 +1221,8 @@ def create(self, gid):
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),
Expand Down Expand Up @@ -1763,7 +1763,7 @@ def reload_configuration(self, gid, sid):

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(
Expand All @@ -1782,7 +1782,7 @@ def reload_configuration(self, gid, sid):

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
Expand Down
28 changes: 27 additions & 1 deletion web/pgadmin/browser/server_groups/servers/static/js/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
35 changes: 35 additions & 0 deletions web/pgadmin/browser/static/js/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 7 additions & 2 deletions web/pgadmin/misc/cloud/static/js/cloud.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', [
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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() {
Expand Down
27 changes: 22 additions & 5 deletions web/pgadmin/misc/properties/ObjectNodeProperties.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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){
Expand All @@ -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.
Expand All @@ -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) {
Expand Down Expand Up @@ -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') {
Expand Down
Loading

0 comments on commit 90b9a86

Please sign in to comment.