Fixed an issue where pgAdmin failed to update the server connection status when the server was disconnected in the background and a refresh was performed on that server. #8149

pull/9010/head
Aditya Toshniwal 2025-07-31 12:43:49 +05:30 committed by GitHub
parent 9eec4f5b8c
commit b2ec3a5acc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 54 additions and 416 deletions

View File

@ -31,7 +31,7 @@ from config import PG_DEFAULT_DRIVER
from pgadmin.model import db, Server, ServerGroup, User, SharedServer
from pgadmin.utils.driver import get_driver
from pgadmin.utils.master_password import get_crypt_key
from pgadmin.utils.exception import CryptKeyMissing
from pgadmin.utils.exception import CryptKeyMissing, ConnectionLost
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
from pgadmin.browser.server_groups.servers.utils import \
(is_valid_ipaddress, get_replication_type, convert_connection_parameter,
@ -627,12 +627,18 @@ class ServerNode(PGChildNodeView):
in_recovery = None
wal_paused = None
if connected:
status, result, in_recovery, wal_paused =\
recovery_state(conn, manager.version)
if not status:
try:
status, result, in_recovery, wal_paused =\
recovery_state(conn, manager.version)
if not status:
connected = False
manager.release()
errmsg = "{0} : {1}".format(server.name, result)
except ConnectionLost:
connected = False
manager.release()
errmsg = "{0} : {1}".format(server.name, result)
return make_json_response(
result=self.blueprint.generate_browser_node(

View File

@ -10,7 +10,6 @@
import MainMenuFactory from './MainMenuFactory';
import _ from 'lodash';
import { checkMasterPassword, showQuickSearch } from '../../../static/js/Dialogs/index';
import { pgHandleItemError } from '../../../static/js/utils';
import { send_heartbeat, stop_heartbeat } from './heartbeat';
import getApiInstance from '../../../static/js/api_instance';
import usePreferences, { setupPreferenceBroadcast } from '../../../preferences/static/js/store';
@ -195,7 +194,7 @@ define('pgadmin.browser', [
obj.check_corrupted_db_file();
obj.Events.on('pgadmin:browser:tree:add', obj.onAddTreeNode.bind(obj));
obj.Events.on('pgadmin:browser:tree:update', obj.onUpdateTreeNode.bind(obj));
obj.Events.on('pgadmin:browser:tree:refresh', obj.onRefreshTreeNodeReact.bind(obj));
obj.Events.on('pgadmin:browser:tree:refresh', obj.onRefreshTreeNode.bind(obj));
obj.Events.on('pgadmin-browser:tree:loadfail', obj.onLoadFailNode.bind(obj));
obj.bind_beforeunload();
@ -1228,173 +1227,39 @@ define('pgadmin.browser', [
}
},
onRefreshTreeNodeReact: function(_i, _opts) {
this.tree.refresh(_i).then(() =>{
if (_opts?.success) _opts.success();
});
},
onRefreshTreeNode: async function(nodeItem, opts) {
this.tree.toggleItemLoader(nodeItem, true);
onRefreshTreeNode: function(_i, _opts) {
let _d = _i && this.tree.itemData(_i),
n = this.Nodes[_d?._type],
ctx = {
b: this, // Browser
d: _d, // current parent
i: _i, // current item
p: null, // path of the old object
pathOfTreeItems: [], // path items
t: this.tree, // Tree Api
o: _opts,
},
isOpen,
idx = -1;
const itemNodeData = nodeItem && this.tree.itemData(nodeItem);
let nodeObj = this.Nodes[itemNodeData?._type];
this.Events.trigger('pgadmin-browser:tree:refreshing', _i, _d, n);
// If the node is a collection node, we can directly refresh it.
if(!nodeObj?.collection_node) {
// If the node is not a collection node, we need to fetch its data
// from the server and update the tree node.
try {
const url = nodeObj.generate_url(nodeItem, 'nodes', itemNodeData, true);
const api = getApiInstance();
const resp = await api.get(url);
// server response data comes in result
const respData = resp.data.data || resp.data.result;
if (!n) {
_i = null;
ctx.i = null;
ctx.d = null;
} else {
isOpen = (this.tree.isInode(_i) && this.tree.isOpen(_i));
}
ctx.branch = ctx.t.serialize(
_i, {}, function(i, el, d) {
idx++;
if (!idx || (d.inode && d.open)) {
return {
_id: d._id, _type: d._type, branch: d.branch, open: d.open,
};
}
});
if (!n) {
ctx.t.destroy({
success: function() {
ctx.t = ctx.b.tree;
ctx.i = null;
ctx.b._refreshNode(ctx, ctx.branch);
},
error: function() {
let fail = _opts.o?.fail || _opts?.fail;
if (typeof(fail) == 'function') {
fail();
}
},
});
return;
}
let api = getApiInstance();
let fetchNodeInfo = function(__i, __d, __n) {
let info = __n.getTreeNodeHierarchy(__i),
url = __n.generate_url(__i, 'nodes', __d, true);
api.get(
url
).then(({data: res})=> {
// Node information can come as result/data
let newData = res.result || res.data;
newData._label = newData.label;
newData.label = _.escape(newData.label);
ctx.t.setLabel(ctx.i, {label: newData.label});
ctx.t.addIcon(ctx.i, {icon: newData.icon});
ctx.t.setId(ctx.i, {id: newData.id});
if (newData.inode)
ctx.t.setInode(ctx.i, {inode: true});
// This will update the tree item data.
let itemData = ctx.t.itemData(ctx.i);
_.extend(itemData, newData);
if (
__n.can_expand && typeof(__n.can_expand) == 'function'
) {
if (!__n.can_expand(itemData)) {
ctx.t.unload(ctx.i);
return;
}
}
ctx.b._refreshNode(ctx, ctx.branch);
let success = (ctx?.o?.success) || ctx.success;
if (success && typeof(success) == 'function') {
success();
}
}).catch(function(error) {
if (!pgHandleItemError(
error, {item: __i, info: info}
)) {
if(error.response.headers['content-type'] == 'application/json') {
let jsonResp = error.response.data ?? {};
if(error.response.status == 410 && jsonResp.success == 0) {
let parent = ctx.t.parent(ctx.i);
ctx.t.remove(ctx.i, {
success: function() {
if (parent) {
// Try to refresh the parent on error
try {
pgBrowser.Events.trigger(
'pgadmin:browser:tree:refresh', parent
);
} catch (e) { console.warn(e.stack || e); }
}
},
});
}
}
pgAdmin.Browser.notifier.pgNotifier('error', error, gettext('Error retrieving details for the node.'), function (msg) {
if (msg == 'CRYPTKEY_SET') {
fetchNodeInfo(__i, __d, __n);
} else {
console.warn(arguments);
}
if(respData) {
this.tree.update(nodeItem, {
...itemNodeData, ...respData
});
this.tree.setLabel(nodeItem, {label: respData.label});
this.tree.addIcon(nodeItem, {icon: respData.icon});
}
});
};
if (n?.collection_node) {
let p = ctx.i = this.tree.parent(_i),
unloadNode = function() {
this.tree.unload(_i, {
success: function() {
_i = p;
_d = ctx.d = ctx.t.itemData(ctx.i);
n = ctx.b.Nodes[_d._type];
_i = p;
fetchNodeInfo(_i, _d, n);
},
fail: function() { console.warn(arguments); },
});
}.bind(this);
if (!this.tree.isInode(_i)) {
this.tree.setInode(_i, { success: unloadNode });
} else {
unloadNode();
} catch (error) {
console.error('Failed to refresh tree node:', error);
return;
}
} else if (isOpen) {
this.tree.unload(_i, {
success: fetchNodeInfo.bind(this, _i, _d, n),
fail: function() {
console.warn(arguments);
},
});
} else if (!this.tree.isInode(_i) && _d.inode) {
this.tree.setInode(_i, {
success: fetchNodeInfo.bind(this, _i, _d, n),
fail: function() {
console.warn(arguments);
},
});
} else {
fetchNodeInfo(_i, _d, n);
}
await this.tree.refresh(nodeItem);
this.tree.toggleItemLoader(nodeItem, false);
opts?.success?.();
},
onLoadFailNode: function(_nodeData) {

View File

@ -152,6 +152,7 @@ export class FileTreeX extends React.Component<IFileTreeXProps> {
resize: this.resize,
showLoader: this.showLoader,
hideLoader: this.hideLoader,
toggleItemLoader: this.toggleItemLoader,
};
model.decorations.addDecoration(this.activeFileDec);
@ -556,6 +557,17 @@ export class FileTreeX extends React.Component<IFileTreeXProps> {
};
private readonly toggleItemLoader = (item: FileOrDir, show=false) => {
const ref = FileTreeItem.itemIdToRefMap.get(item.id);
if (ref) {
if (show) {
this.showLoader(ref);
} else {
this.hideLoader(ref);
}
}
};
private readonly showLoader = (ref: HTMLDivElement) => {
// get label ref and add loading class
ref.style.background = 'none';

View File

@ -1,249 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import * as BrowserFS from 'browserfs';
import pgAdmin from 'sources/pgadmin';
import _ from 'lodash';
import { FileType } from 'react-aspen';
import { findInTree } from './tree';
export class ManagePreferenceTreeNodes {
constructor(data) {
this.tree = {};
this.tempTree = new TreeNode(undefined, {});
this.treeData = data || [];
}
public init = (_root: string) => new Promise((res) => {
const node = { parent: null, children: [], data: null };
this.tree = {};
this.tree[_root] = { name: 'root', type: FileType.Directory, metadata: node };
res();
});
public updateNode = (_path, _data) => new Promise((res) => {
const item = this.findNode(_path);
if (item) {
item.name = _data.label;
item.metadata.data = _data;
}
res(true);
});
public removeNode = async (_path) => {
const item = this.findNode(_path);
if (item?.parentNode) {
item.children = [];
item.parentNode.children.splice(item.parentNode.children.indexOf(item), 1);
}
return true;
};
findNode(path) {
if (path === null || path === undefined || path.length === 0 || path == '/preferences') {
return this.tempTree;
}
return findInTree(this.tempTree, path);
}
public addNode = (_parent: string, _path: string, _data: []) => new Promise((res) => {
_data.type = _data.inode ? FileType.Directory : FileType.File;
_data._label = _data.label;
_data.label = _.escape(_data.label);
_data.is_collection = isCollectionNode(_data._type);
const nodeData = { parent: _parent, children: _data?.children ? _data.children : [], data: _data };
const tmpParentNode = this.findNode(_parent);
const treeNode = new TreeNode(_data.id, _data, {}, tmpParentNode, nodeData, _data.type);
if (tmpParentNode !== null && tmpParentNode !== undefined) tmpParentNode.children.push(treeNode);
res(treeNode);
});
public readNode = (_path: string) => new Promise<string[]>((res, rej) => {
const temp_tree_path = _path,
node = this.findNode(_path);
node.children = [];
if (node && node.children.length > 0) {
if (!node.type === FileType.File) {
rej(new Error('It\'s a leaf node'));
}
else if (node?.children.length != 0) {
res(node.children);
}
}
const self = this;
async function loadData() {
const Path = BrowserFS.BFSRequire('path');
const fill = async (tree) => {
for (const idx in tree) {
const _node = tree[idx];
const _pathl = Path.join(_path, _node.id);
await self.addNode(temp_tree_path, _pathl, _node);
}
};
if (node && !_.isUndefined(node.id)) {
const _data = self.treeData.find((el) => el.id == node.id);
const subNodes = [];
_data.childrenNodes.forEach(element => {
subNodes.push(element);
});
await fill(subNodes);
} else {
await fill(self.treeData);
}
self.returnChildrens(node, res);
}
loadData();
});
public returnChildrens = (node: any, res: any) =>{
if (node?.children.length > 0) return res(node.children);
else return res(null);
};
}
export class TreeNode {
constructor(id, data, domNode, parent, metadata, type) {
this.id = id;
this.data = data;
this.setParent(parent);
this.children = [];
this.domNode = domNode;
this.metadata = metadata;
this.name = metadata ? metadata.data.label : '';
this.type = type || undefined;
}
hasParent() {
return this.parentNode !== null && this.parentNode !== undefined;
}
parent() {
return this.parentNode;
}
setParent(parent) {
this.parentNode = parent;
this.path = this.id;
if (this.id)
if (parent !== null && parent !== undefined && parent.path !== undefined) {
this.path = parent.path + '/' + this.id;
} else {
this.path = '/preferences/' + this.id;
}
}
getData() {
if (this.data === undefined) {
return undefined;
} else if (this.data === null) {
return null;
}
return {...this.data};
}
getHtmlIdentifier() {
return this.domNode;
}
/*
* Find the ancestor with matches this condition
*/
ancestorNode(condition) {
let node;
while (this.hasParent()) {
node = this.parent();
if (condition(node)) {
return node;
}
}
return null;
}
/**
* Given a condition returns true if the current node
* or any of the parent nodes condition result is true
*/
anyFamilyMember(condition) {
if (condition(this)) {
return true;
}
return this.ancestorNode(condition) !== null;
}
anyParent(condition) {
return this.ancestorNode(condition) !== null;
}
reload(tree) {
return new Promise((resolve) => {
this.unload(tree)
.then(() => {
tree.setInode(this.domNode);
tree.deselect(this.domNode);
setTimeout(() => {
tree.selectNode(this.domNode);
}, 0);
resolve();
});
});
}
unload(tree) {
return new Promise((resolve, reject) => {
this.children = [];
tree.unload(this.domNode)
.then(
() => {
resolve(true);
},
() => {
reject(new Error());
});
});
}
open(tree, suppressNoDom) {
return new Promise((resolve, reject) => {
if (suppressNoDom && (this.domNode == null || typeof (this.domNode) === 'undefined')) {
resolve(true);
} else if (tree.isOpen(this.domNode)) {
resolve(true);
} else {
tree.open(this.domNode).then(() => resolve(true), () => reject(new Error(true)));
}
});
}
}
export function isCollectionNode(node) {
if (pgAdmin.Browser.Nodes && node in pgAdmin.Browser.Nodes) {
if (pgAdmin.Browser.Nodes[node].is_collection !== undefined) return pgAdmin.Browser.Nodes[node].is_collection;
else return false;
}
return false;
}

View File

@ -583,6 +583,10 @@ export class Tree {
onNodeCopy(copyCallback) {
this.copyHandler = copyCallback;
}
toggleItemLoader(item, show) {
this.tree.toggleItemLoader(item, show);
}
}
function mapType(type, idx) {