pgadmin4/web/pgadmin/browser/static/js/node.js

1243 lines
40 KiB
JavaScript

/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2023, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import {getNodeView, removeNodeView} from './node_view';
import Notify from '../../../static/js/helpers/Notifier';
import _ from 'lodash';
import getApiInstance from '../../../static/js/api_instance';
import { removePanelView } from './panel_view';
import { TAB_CHANGE } from './constants';
define('pgadmin.browser.node', [
'sources/gettext', 'sources/pgadmin',
'sources/browser/generate_url', 'sources/utils',
'pgadmin.browser.utils', 'pgadmin.browser.events',
], function(
gettext, pgAdmin, generateUrl, commonUtils
) {
let wcDocker = window.wcDocker;
const pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {};
// It has already been defined.
// Avoid running this script again.
if (pgBrowser.Node)
return pgBrowser.Node;
pgBrowser.Nodes = pgBrowser.Nodes || {};
// A helper (base) class for all the nodes, this has basic
// operations/callbacks defined for basic operation.
pgBrowser.Node = function() {/*This is intentional (SonarQube)*/};
// Helper function to correctly set up the property chain, for subclasses.
// Uses a hash of class properties to be extended.
//
// It is unlikely - we will instantiate an object for this class.
pgBrowser.Node.extend = function(props, initialize) {
let parent = this;
let child;
// The constructor function for the new subclass is defined to simply call
// the parent's constructor.
child = function() {
return parent.apply(this, arguments);
};
// Add static properties to the constructor function, if supplied.
_.extend(child, parent, _.omit(props, 'callbacks'));
// Make sure - a child have all the callbacks of the parent.
child.callbacks = _.extend({}, parent.callbacks, props.callbacks);
// Let's not bind the callbacks, or initialize the child.
if (!(initialize??true))
return child;
let bindToChild = function(cb) {
if (typeof(child.callbacks[cb]) == 'function') {
child.callbacks[cb] = child.callbacks[cb].bind(child);
}
},
callbacks = _.keys(child.callbacks);
for (let cb_val of callbacks) bindToChild(cb_val);
// Registering the node by calling child.Init(...) function
child.Init.apply(child);
// Initialize the parent
this.Init.apply(child);
return child;
};
_.extend(pgAdmin.Browser.Node, {
// Node type
type: undefined,
// Label
label: '',
// Help pages
sqlAlterHelp: '',
sqlCreateHelp: '',
dialogHelp: '',
epasHelp: false,
title: function(o, d) {
return o.label + (d ? (' - ' + d.label) : '');
},
hasId: true,
///////
// Initialization function
// Generally - used to register the menus for this type of node.
//
// Also, look at pgAdmin.Browser.add_menus(...) function.
//
// NOTE: Override this for each node for initialization purpose
Init: function() {
let self = this;
if (self.node_initialized)
return;
self.node_initialized = true;
pgAdmin.Browser.add_menus([{
name: 'refresh',
node: self.type,
module: self,
applies: ['object', 'context'],
callback: 'refresh',
priority: 2,
label: gettext('Refresh...'),
enable: true,
}]);
if (self.canEdit) {
pgAdmin.Browser.add_menus([{
name: 'show_obj_properties',
node: self.type,
module: self,
applies: ['object', 'context'],
callback: 'show_obj_properties',
priority: 999,
label: gettext('Properties...'),
data: {
'action': 'edit',
},
enable: _.isFunction(self.canEdit) ?
function() {
return !!(self.canEdit.apply(self, arguments));
} : (!!self.canEdit),
}]);
}
if (self.canDrop) {
pgAdmin.Browser.add_menus([{
name: 'delete_object',
node: self.type,
module: self,
applies: ['object', 'context'],
callback: 'delete_obj',
priority: self.dropPriority,
label: (self.dropAsRemove) ? gettext('Remove %s', self.label) : gettext('Delete/Drop'),
data: {
'url': 'drop',
data_disabled: gettext('The selected tree node does not support this option.'),
},
enable: _.isFunction(self.canDrop) ?
function() {
return !!(self.canDrop.apply(self, arguments));
} : (!!self.canDrop),
}]);
if (self.canDropCascade) {
pgAdmin.Browser.add_menus([{
name: 'delete_object_cascade',
node: self.type,
module: self,
applies: ['object', 'context'],
callback: 'delete_obj',
priority: 3,
label: gettext('Drop Cascade'),
data: {
'url': 'delete',
},
enable: _.isFunction(self.canDropCascade) ?
function() {
return self.canDropCascade.apply(self, arguments);
} : (!!self.canDropCascade),
}]);
}
}
// Show query tool only in context menu of supported nodes.
if (_.indexOf(pgAdmin.unsupported_nodes, self.type) == -1) {
let enable = function(itemData) {
if (itemData?._type == 'database' && itemData?.allowConn)
return true;
else if (itemData?._type != 'database')
return true;
else
return false;
};
pgAdmin.Browser.add_menus([{
name: 'show_query_tool',
node: self.type,
module: self,
applies: ['context'],
callback: 'show_query_tool',
priority: 998,
label: gettext('Query Tool'),
enable: enable,
}]);
// show search objects same as query tool
pgAdmin.Browser.add_menus([{
name: 'search_objects', node: self.type, module: pgAdmin.Tools.SearchObjects,
applies: ['context'], callback: 'show_search_objects',
priority: 997, label: gettext('Search Objects...'),
icon: 'fa fa-search', enable: enable,
}]);
if(pgAdmin['enable_psql']) {
// show psql tool same as query tool.
pgAdmin.Browser.add_menus([{
name: 'show_psql_tool', node: this.type, module: this,
applies: ['context'], callback: 'show_psql_tool',
priority: 998, label: gettext('PSQL Tool'),
}]);
}
}
// This will add options of scripts eg:'CREATE Script'
if (self.hasScriptTypes && _.isArray(self.hasScriptTypes) &&
self.hasScriptTypes.length > 0) {
// For each script type create menu
_.each(self.hasScriptTypes, function(stype) {
let type_label = gettext('%s Script',stype.toUpperCase());
stype = stype.toLowerCase();
// Adding menu for each script type
pgAdmin.Browser.add_menus([{
name: 'show_script_' + stype,
node: self.type,
module: self,
applies: ['object', 'context'],
callback: 'show_script',
priority: 4,
label: type_label,
category: gettext('Scripts'),
data: {
'script': stype,
data_disabled: gettext('The selected tree node does not support this option.'),
},
enable: self.check_user_permission,
}]);
});
}
},
///////
// Checks if Script Type is allowed to user
// First check if role node & create role allowed
// Otherwise test rest of database objects
// if no permission matched then do not allow create script
///////
check_user_permission: function(itemData, item, data) {
// Do not display CREATE script on server group and server node
if (itemData._type == 'server_group' || itemData._type == 'server') {
return false;
}
// Do not display the menu if the database connection is not allowed
if (itemData._type == 'database' && !itemData.allowConn)
return false;
let parentData = pgBrowser.tree.getTreeNodeHierarchy(item);
if (_.indexOf(['create', 'insert', 'update', 'delete'], data.script) != -1) {
if (itemData.type == 'role' &&
parentData.server.user.can_create_role) {
return true;
} else if (
(
parentData.server && (
parentData.server.user.is_superuser ||
parentData.server.user.can_create_db)
) ||
(
parentData.schema && parentData.schema.can_create
)
) {
return true;
} else {
return false;
}
} else {
return true;
}
},
addUtilityPanel: function(width, height, docker) {
let body = window.document.body,
el = document.createElement('div'),
dockerObject = docker || pgBrowser.docker;
body.insertBefore(el, body.firstChild);
let new_width = screen.width < 700 ? screen.width * 0.95 : screen.width * 0.5,
new_height = screen.height < 500 ? screen.height * 0.95 : screen.height * 0.4;
if (!_.isUndefined(width) && !_.isNull(width)) {
new_width = width;
}
if (!_.isUndefined(height) && !_.isNull(height)) {
new_height = height;
}
let x = (body.offsetWidth - new_width) / 2;
let y = (body.offsetHeight - new_height) / 4;
let new_panel = dockerObject.addPanel(
'utility_props', window.wcDocker.DOCK.FLOAT, undefined, {
w: new_width,
h: new_height,
x: (x),
y: (y),
}
);
/*set movable false to prevent dialog from docking,
by setting this we can able to move the dialog but can't dock it
in to the frame. e.g: can't dock it in to properties and other tabs. */
setTimeout(function() {
new_panel.moveable(false);
}, 0);
body.removeChild(el);
return new_panel;
},
registerDockerPanel: function(docker, name, params) {
let w = docker || pgBrowser.docker,
p = w.findPanels(name);
if (p && p.length == 1)
return;
p = new pgBrowser.Panel({
name: name,
showTitle: true,
isCloseable: true,
isPrivate: true,
isLayoutMember: false,
canMaximise: true,
content: '<div class="obj_properties container-fluid h-100"></div>',
...params,
});
p.load(w);
},
registerUtilityPanel: function(docker) {
let w = docker || pgBrowser.docker,
p = w.findPanels('utility_props');
if (p && p.length == 1)
return;
let events = {};
p = new pgBrowser.Panel({
name: 'utility_props',
showTitle: true,
isCloseable: true,
isPrivate: true,
isLayoutMember: false,
canMaximise: true,
elContainer: true,
content: '<div class="obj_properties"></div>',
onCreate: function(myPanel, container) {
container.classList.add('pg-no-overflow');
},
events: events,
});
p.load(w);
},
register_node_panel: function() {
let w = pgBrowser.docker,
p = w.findPanels('node_props');
if (p && p.length == 1)
return;
let events = {};
p = new pgBrowser.Panel({
name: 'node_props',
showTitle: true,
isCloseable: true,
isPrivate: true,
isLayoutMember: false,
canMaximise: true,
elContainer: true,
content: '<div class="obj_properties"></div>',
onCreate: function(myPanel, container) {
container.classList.add('pg-no-overflow');
},
events: events,
});
p.load(pgBrowser.docker);
},
/*
* Default script type menu for node.
*
* Override this, to show more script type menus (e.g hasScriptTypes: ['create', 'select', 'insert', 'update', 'delete'])
*
* Or set it to empty array to disable script type menu on node (e.g hasScriptTypes: [])
*/
hasScriptTypes: ['create'],
/******************************************************************
* This function determines the given item is editable or not.
*
* Override this, when a node is not editable.
*/
canEdit: true,
/******************************************************************
* This function determines the given item is deletable or not.
*
* Override this, when a node is not deletable.
*/
canDrop: false,
/************************************************************************
* This function determines the given item and children are deletable or
* not.
*
* Override this, when a node is not deletable.
*/
canDropCascade: false,
/*********************************************************************************
dropAsRemove should be true in case, Drop object label needs to be replaced by Remove
*/
dropAsRemove: false,
/******************************************************************************
dropPriority is set to 2 by default, override it when change is required
*/
dropPriority: 2,
/******************************************************************************
select collection node on deletion.
*/
selectParentNodeOnDelete: false,
// List of common callbacks - that can be used for different
// operations!
callbacks: {
/******************************************************************
* This function allows to create/edit/show properties of any
* object depending on the arguments provided.
*
* args must be a object containing:
* action - create/edit/properties
* item - The properties of the item (tree node item)
*
* NOTE:
* if item is not provided, the action will be done on the
* currently selected tree item node.
*
**/
show_obj_properties: function(args, item) {
let t = pgBrowser.tree,
i = (args && args.item) || item || t.selected(),
d = i ? t.itemData(i) : undefined,
o = this,
l = o.title.apply(this, [d]),
p;
// Make sure - the properties dialog type registered
pgBrowser.Node.register_node_panel();
// No node selected.
if (!d)
return;
let self = this,
isParent = (_.isArray(this.parent_type) ?
function(_d) {
return (_.indexOf(self.parent_type, _d._type) != -1);
} : function(_d) {
return (self.parent_type == _d._type);
}),
addPanel = function() {
let body = window.document.body,
el = document.createElement('div');
body.insertBefore(el, body.firstChild);
let w, h, x, y;
if(screen.width < 800) {
w = pgAdmin.toPx(el, '95%', 'width', true);
} else {
w = pgAdmin.toPx(
el, self.width || (pgBrowser.stdW.default + 'px'),
'width', true
);
/* Fit to standard sizes */
if(w <= pgBrowser.stdW.sm) {
w = pgBrowser.stdW.sm;
} else {
if(w <= pgBrowser.stdW.md) {
w = pgBrowser.stdW.md;
} else {
w = pgBrowser.stdW.lg;
}
}
}
if(screen.height < 600) {
h = pgAdmin.toPx(el, '95%', 'height', true);
} else {
h = pgAdmin.toPx(
el, self.height || (pgBrowser.stdH.default + 'px'),
'height', true
);
/* Fit to standard sizes */
if(h <= pgBrowser.stdH.sm) {
h = pgBrowser.stdH.sm;
} else {
if(h <= pgBrowser.stdH.md) {
h = pgBrowser.stdH.md;
} else {
h = pgBrowser.stdH.lg;
}
}
}
x = (body.offsetWidth - w) / 2;
y = (body.offsetHeight - h) / 4;
// If the screen resolution is higher, but - it is zoomed, dialog
// may be go out of window, and will not be accessible through the
// keyboard.
if (w > window.innerWidth) {
x = 0;
w = window.innerWidth;
}
if (h > window.innerHeight) {
y = 0;
h = window.innerHeight;
}
let new_panel = pgBrowser.docker.addPanel(
'node_props', wcDocker.DOCK.FLOAT, undefined, {
w: w + 'px',
h: h + 'px',
x: x + 'px',
y: y + 'px',
}
);
body.removeChild(el);
return new_panel;
};
if (args.action == 'create') {
// If we've parent, we will get the information of it for
// proper object manipulation.
//
// You know - we're working with RDBMS, relation is everything
// for us.
if (self.parent_type && !isParent(d)) {
// In browser tree, I can be under any node, But - that
// does not mean, it is my parent.
//
// We have some group nodes too.
//
// i.e.
// Tables, Views, etc. nodes under Schema node
//
// And, actual parent of a table is schema, not Tables.
while (i && t.hasParent(i)) {
i = t.parent(i);
let pd = t.itemData(i);
if (isParent(pd)) {
// Assign the data, this is my actual parent.
d = pd;
break;
}
}
}
// Seriously - I really don't have parent data present?
//
// The only node - which I know - who does not have parent
// node, is the Server Group (and, comes directly under root
// node - which has no parent.)
if (!d || (this.parent_type != null && !isParent(d))) {
// It should never come here.
// If it is here, that means - we do have some bug in code.
return;
}
l = gettext('Create - %s', this.label);
if (this.type == 'server') {
l = gettext('Register - %s', this.label);
}
p = addPanel();
setTimeout(function() {
o.showProperties(i, d, p, args.action);
}, 10);
} else {
if (pgBrowser.Node.panels && pgBrowser.Node.panels[d.id] &&
pgBrowser.Node.panels[d.id].$container) {
p = pgBrowser.Node.panels[d.id];
/** TODO ::
* Run in edit mode (if asked) only when it is
* not already been running edit mode
**/
let mode = p.$container.attr('action-mode');
if (mode) {
let msg = gettext('Are you sure want to stop editing the properties of %s "%s"?');
if (args.action == 'edit') {
msg = gettext('Are you sure want to reset the current changes and re-open the panel for %s "%s"?');
}
Notify.confirm(
gettext('Edit in progress?'),
commonUtils.sprintf(msg, o.label.toLowerCase(), d.label),
function() {
setTimeout(function() {
o.showProperties(i, d, p, args.action);
}, 10);
},
null).show();
} else {
setTimeout(function() {
o.showProperties(i, d, p, args.action);
}, 10);
}
} else {
pgBrowser.Node.panels = pgBrowser.Node.panels || {};
p = pgBrowser.Node.panels[d.id] = addPanel();
setTimeout(function() {
o.showProperties(i, d, p, args.action);
}, 10);
}
}
p.title(l);
p.icon('icon-' + this.type);
// Make sure the properties dialog is visible
p.focus();
},
// Delete the selected object
delete_obj: function(args, item) {
let input = args || {
'url': 'drop',
},
obj = this,
t = pgBrowser.tree,
i = input.item || item || t.selected(),
d = i ? t.itemData(i) : undefined;
if (!d)
return;
/*
* Make sure - we're using the correct version of node
*/
obj = pgBrowser.Nodes[d._type];
let objName = _.unescape(d.label);
let msg, title;
if (input.url == 'delete') {
msg = gettext('Are you sure you want to drop %s "%s" and all the objects that depend on it?',
obj.label.toLowerCase(), d.label);
title = gettext('DROP CASCADE %s?', obj.label);
if (!(_.isFunction(obj.canDropCascade) ?
obj.canDropCascade.apply(obj, [d, i]) : obj.canDropCascade)) {
Notify.error(
gettext('The %s "%s" cannot be dropped.', obj.label, d.label),
10000
);
return;
}
} else {
if (obj.dropAsRemove) {
msg = gettext('Are you sure you want to remove %s "%s"?', obj.label.toLowerCase(), d.label);
title = gettext('Remove %s?', obj.label);
} else {
msg = gettext('Are you sure you want to drop %s "%s"?', obj.label.toLowerCase(), d.label);
title = gettext('Drop %s?', obj.label);
}
if (!(_.isFunction(obj.canDrop) ?
obj.canDrop.apply(obj, [d, i]) : obj.canDrop)) {
Notify.error(
gettext('The %s "%s" cannot be dropped/removed.', obj.label, d.label),
10000
);
return;
}
}
Notify.confirm(title, msg,
function() {
getApiInstance().delete(
obj.generate_url(i, input.url, d, true),
).then(({data: res})=> {
if(res.success == 2){
Notify.error(res.info, null);
return;
}
if (res.success == 0) {
Notify.alert(res.errormsg, res.info);
} else {
// Remove the node from tree and set collection node as selected.
let selectNextNode = true;
if(obj.selectParentNodeOnDelete) {
let prv_i = t.parent(i);
setTimeout(function() {
t.select(prv_i);
}, 10);
selectNextNode = false;
}
pgBrowser.removeTreeNode(i, selectNextNode);
}
return true;
}).catch(function(error) {
let errmsg = error.request?.responseText;
/* Error from the server */
if (error.request?.status == 417 || error.request?.status == 410 || error.request?.status == 500) {
try {
let data = error.response.data;
errmsg = data.info || data.errormsg;
} catch (e) {
console.warn(e.stack || e);
}
}
Notify.alert(gettext('Error dropping/removing %s: "%s"', obj.label, objName), errmsg);
});
}
);
},
// Callback for creating script(s) & opening them in Query editor
show_script: function(args, item) {
let scriptType = args.script,
obj,
t = pgBrowser.tree,
i = item || t.selected(),
d = i ? t.itemData(i) : undefined;
if (!d)
return;
/*
* Make sure - we're using the correct version of node
*/
obj = pgBrowser.Nodes[d._type];
let sql_url;
// URL for script type
if (scriptType == 'insert') {
sql_url = 'insert_sql';
} else if (scriptType == 'update') {
sql_url = 'update_sql';
} else if (scriptType == 'delete') {
sql_url = 'delete_sql';
} else if (scriptType == 'select') {
sql_url = 'select_sql';
} else if (scriptType == 'exec') {
sql_url = 'exec_sql';
} else {
// By Default get CREATE SQL
sql_url = 'sql';
}
// Open data grid & pass the URL for fetching
pgAdmin.Tools.SQLEditor.showQueryTool(
obj.generate_url(i, sql_url, d, true),
i, scriptType
);
},
// Callback to render query editor
show_query_tool: function(args, item) {
let preference = pgBrowser.get_preference('sqleditor', 'copy_sql_to_query_tool');
let t = pgBrowser.tree,
i = item || t.selected(),
d = i ? t.itemData(i) : undefined;
if (!d)
return;
// Here call data grid method to render query tool
//Open query tool with create script if copy_sql_to_query_tool is true else open blank query tool
if(preference.value && !d._type.includes('coll-')){
let stype = d._type.toLowerCase();
let data = {
'script': stype,
data_disabled: gettext('The selected tree node does not support this option.'),
};
pgBrowser.Node.callbacks.show_script(data);
}else{
pgAdmin.Tools.SQLEditor.showQueryTool('', i);
}
},
// Callback to render psql tool.
show_psql_tool: function(args) {
let input = args || {},
t = pgBrowser.tree,
i = input.item || t.selected(),
d = i ? t.itemData(i) : undefined;
pgBrowser.psql.psql_tool(d, i, true);
},
// Logic to change the server background colour
// There is no way of applying CSS to parent element so we have to
// do it via JS code only
change_server_background: function(item, data) {
if (!item || !data)
return;
const treeH = pgBrowser.tree.getTreeNodeHierarchy(item);
const serverData = treeH['server'];
if (!serverData) {
return;
}
const index = item.path.indexOf(serverData.id);
// Go further only if node type is a Server
if (index !== -1) {
// First element will be icon and second will be colour code
let bgcolor = serverData.icon.split(' ')[1] || null,
fgcolor = serverData.icon.split(' ')[2] || '';
if (bgcolor) {
let dynamic_class = 'pga_server_' + serverData._id + '_bgcolor';
// Prepare dynamic style tag
const styleTag = document.createElement('style');
styleTag.setAttribute('id', dynamic_class);
styleTag.setAttribute('type', 'text/css');
styleTag.innerText = `
.${dynamic_class} .file-label {
border-radius: 3px;
margin-bottom: 2px;
background: ${bgcolor} !important;
}
${fgcolor ? `
.${dynamic_class} span.file-name {
color: ${fgcolor} !important;
}
`:''}
`;
// Prepare dynamic style tag using template
document.querySelector(`style[id="${dynamic_class}"]`)?.remove();
document.head.appendChild(styleTag);
// Add dynamic class to the tree node.
pgBrowser.tree.addCssClass(item, dynamic_class);
}
}
},
added: function(item, data) {
if (pgBrowser.tree.getData(item)._type.indexOf('coll-') !== -1){
setTimeout(function() {
let _item = pgAdmin.Browser.Nodes[pgBrowser.tree.getData(item).nodes[0]];
_item.clear_cache.apply(_item);
}, 0);
}
pgBrowser.Events.trigger('pgadmin:browser:tree:expand-from-previous-tree-state',
item);
pgBrowser.Node.callbacks.change_server_background(item, data);
},
// Callback called - when a node is selected in browser tree.
selected: function(item, data, browser, _argsList, _event, actionSource) {
// Show the information about the selected node in the below panels,
// which are visible at this time:
// + Properties
// + Query (if applicable, otherwise empty)
// + Dependents
// + Dependencies
// + Statistics
let b = browser || pgBrowser,
t = b.tree,
d = data || t.itemData(item);
// Update the menu items
pgAdmin.Browser.enable_disable_menus.apply(b, [item]);
if (d && b) {
if ('properties' in b.panels &&
b.panels['properties'] &&
b.panels['properties'].panel) {
if (actionSource != TAB_CHANGE) {
const propertiesPanel = b.panels['properties'].panel.$container.find('.obj_properties').first();
if (propertiesPanel) {
removePanelView(propertiesPanel[0]);
}
}
if (b.panels['properties'].panel.isVisible()) {
this.showProperties(item, d, b.panels['properties'].panel);
}
}
}
pgBrowser.Events.trigger('pgadmin:browser:tree:update-tree-state',
item);
return true;
},
removed: function(item) {
let self = this;
setTimeout(function() {
self.clear_cache.apply(self, item);
}, 0);
},
refresh: function(cmd, _item) {
let self = this,
t = pgBrowser.tree,
data = _item && t.itemData(_item);
pgBrowser.Events.trigger(
'pgadmin:browser:tree:refresh', _item || pgBrowser.tree.selected(), {
success: function() {
self.callbacks.selected.apply(self, [_item, data, pgBrowser]);
},
});
},
opened: function(item) {
let tree = pgBrowser.tree,
auto_expand = pgBrowser.get_preference('browser', 'auto_expand_sole_children');
if (auto_expand && auto_expand.value && tree.children(item).length == 1) {
// Automatically expand the child node, if a treeview node has only a single child.
const first_child = tree.first(item);
if (first_child._loaded) {
tree.open(first_child);
tree.select(first_child);
} else {
const openSoleItem = setInterval(() => {
if (first_child._loaded) {
tree.open(first_child);
tree.select(first_child);
clearSoleItemInterval();
}
}, 200);
const clearSoleItemInterval = function() {
clearInterval(openSoleItem);
};
}
} else if(tree.children(item).length == 1) {
const first_child = tree.first(item);
tree.select(first_child);
}
pgBrowser.Events.trigger('pgadmin:browser:tree:update-tree-state', item);
},
closed: function(item) {
pgBrowser.Events.trigger('pgadmin:browser:tree:remove-from-tree-state',
item);
},
},
/**********************************************************************
* A hook (not a callback) to show object properties in given HTML
* element.
*
* This has been used for the showing, editing properties of the node.
* This has also been used for creating a node.
**/
showProperties: function(item, data, panel, action) {
let that = this,
j = panel.$container.find('.obj_properties').first();
// Callback to show object properties
let properties = function() {
let treeNodeInfo = pgBrowser.tree.getTreeNodeHierarchy(item);
getNodeView(
that.type, treeNodeInfo, 'properties', data, 'tab', j[0], this, onEdit
);
return;
}.bind(panel),
editFunc = function() {
let self = this;
if (action && action == 'properties') {
action = 'edit';
}
self.$container.attr('action-mode', action);
self.icon(
_.isFunction(that['node_image']) ?
(that['node_image']).apply(that, [data]) :
(that['node_image'] || ('icon-' + that.type))
);
/* Remove any dom rendered by getNodeView */
removeNodeView(j[0]);
/* getSchema is a schema for React. Get the react node view */
let treeNodeInfo = pgBrowser.tree.getTreeNodeHierarchy(item);
getNodeView(
that.type, treeNodeInfo, action, data, 'dialog', j[0], this, onEdit,
(nodeData)=>{
if(nodeData.node) {
onSaveFunc(nodeData.node, treeNodeInfo);
if(nodeData.success === 0) {
Notify.alert(gettext('Error'),
gettext(nodeData.errormsg)
);
}
}
}
);
return;
}.bind(panel),
updateTreeItem = function(obj, tnode, node_info) {
let _old = data,
_new = tnode,
info = node_info;
// Clear the cache for this node now.
setTimeout(function() {
obj.clear_cache.apply(obj, item);
}, 0);
pgBrowser.Events.trigger(
'pgadmin:browser:tree:update',
_old, _new, info, {
success: function(_item, _newNodeData, _oldNodeData) {
pgBrowser.Events.trigger(
'pgadmin:browser:node:updated', _item, _newNodeData,
_oldNodeData
);
pgBrowser.Events.trigger(
'pgadmin:browser:node:' + _newNodeData._type + ':updated',
_item, _newNodeData, _oldNodeData
);
},
}
);
this.close();
},
saveNewNode = function(obj, tnode, node_info) {
// Clear the cache for this node now.
setTimeout(function() {
obj.clear_cache.apply(obj, item);
}, 0);
try {
pgBrowser.Events.trigger(
'pgadmin:browser:tree:add', _.clone(tnode),
_.clone(node_info)
);
} catch (e) {
console.warn(e.stack || e);
}
this.close();
}.bind(panel, that),
editInNewPanel = function() {
// Open edit in separate panel
setTimeout(function() {
that.callbacks.show_obj_properties.apply(that, [{
'action': 'edit',
'item': item,
}]);
}, 0);
},
onSaveFunc = updateTreeItem.bind(panel, that),
onEdit = editFunc.bind(panel);
if (action) {
if (action == 'create') {
onSaveFunc = saveNewNode;
}
if (action != 'properties') {
// We need to keep track edit/create mode for this panel.
editFunc();
} else {
properties();
}
} else {
/* Show properties */
onEdit = editInNewPanel.bind(panel);
properties();
}
},
_find_parent_node: function(t, i, d) {
if (this.parent_type) {
d = d || t.itemData(i);
if (_.isString(this.parent_type)) {
if (this.parent_type == d._type) {
return i;
}
while (t.hasParent(i)) {
i = t.parent(i);
d = t.itemData(i);
if (this.parent_type == d._type)
return i;
}
} else {
if (_.indexOf(this.parent_type, d._type) >= 0) {
return i;
}
while (t.hasParent(i)) {
i = t.parent(i);
d = t.itemData(i);
if (_.indexOf(this.parent_type, d._type) >= 0)
return i;
}
}
}
return null;
},
/**********************************************************************
* Generate the URL for different operations
*
* arguments:
* type: Create/drop/edit/properties/sql/depends/statistics
* d: Provide the ItemData for the current item node
* with_id: Required id information at the end?
* jump_after_node: This will skip all the value between jump_after_node
* to the last node, excluding jump_after_node and the last node. This is particularly
* helpful in partition table where we need to skip parent table OID of a partitioned
* table in URL formation. Partitioned table itself is a "table" and can be multilevel
* Supports url generation for create, drop, edit, properties, sql,
* depends, statistics
*/
generate_url: function(item, type, d, with_id, info, jump_after_node) {
let opURL = {
'create': 'obj',
'drop': 'obj',
'edit': 'obj',
'properties': 'obj',
'statistics': 'stats',
},
self = this,
priority = -Infinity;
let treeInfo = (_.isUndefined(item) || _.isNull(item)) ?
info || {} : pgBrowser.tree.getTreeNodeHierarchy(item);
let actionType = type in opURL ? opURL[type] : type;
let itemID = with_id && d._type == self.type ? encodeURIComponent(d._id) : '';
if (self.parent_type) {
if (_.isString(self.parent_type)) {
let p = treeInfo[self.parent_type];
if (p) {
priority = p.priority;
}
} else {
_.each(self.parent_type, function(o) {
let p = treeInfo[o];
if (p) {
if (priority < p.priority) {
priority = p.priority;
}
}
});
}
}
let jump_after_priority = priority;
if(jump_after_node && treeInfo[jump_after_node]) {
jump_after_priority = treeInfo[jump_after_node].priority;
}
let nodePickFunction = function(treeInfoValue) {
return (treeInfoValue.priority <= jump_after_priority || treeInfoValue.priority == priority);
};
return generateUrl.generate_url(pgBrowser.URL, treeInfo, actionType, self.type, nodePickFunction, itemID);
},
cache: function(url, node_info, level, data) {
let cached = this.cached = this.cached || {},
hash = url,
min_priority = (
node_info && node_info[level] && node_info[level].priority
) || 0;
if (node_info) {
_.each(_.sortBy(_.values(_.pickBy(
node_info,
function(v) {
return (v.priority <= min_priority);
}
)), function(o) {
return o.priority;
}), function(o) {
hash = commonUtils.sprintf('%s_%s', hash, encodeURI(o._id));
});
}
if (_.isUndefined(data)) {
let res = cached[hash];
if (!_.isUndefined(res) &&
(res.at - Date.now() > 300000)) {
res = undefined;
}
return res;
}
let res = cached[hash] = {
data: data,
at: Date.now(),
level: level,
};
return res;
},
clear_cache: function(item) {
/*
* Reset the cache, when new node is created.
*
* FIXME:
* At the moment, we will clear all the cache for this node. But - we
* would like to clear the cache only this nodes parent, so that - it
* fetches the new data.
*/
this.cached = {};
// Trigger Notify event about node's cache
let self = this;
pgBrowser.Events.trigger(
'pgadmin:browser:node:' + self.type + ':cache_cleared',
item, self
);
},
cache_level: function(node_info, with_id) {
if (node_info) {
if (with_id && this.type in node_info) {
return this.type;
}
if (_.isArray(this.parent_type)) {
for (let parent in this.parent_type) {
if (parent in node_info) {
return parent;
}
}
return this.type;
}
return this.parent_type;
}
},
});
return pgAdmin.Browser.Node;
});